Skip to content

fix(distribute): drop num_cpus for std::thread::available_parallelism — the 7 selections the tokio gate leaves red - #3266

Merged
noahgift merged 5 commits into
mainfrom
PMAT-1098-numcpus
Sep 16, 2026
Merged

noahgift merged 5 commits into
mainfrom
PMAT-1098-numcpus

Conversation

@noahgift

Copy link
Copy Markdown
Member

What

The seven aprender-distribute selections that #3257's feature = "tokio" gate leaves red. Measured against that PR's actual head (f18e257dc): 17 of 24 single-feature selections go green and these seven stay red with

error[E0433]: failed to resolve: use of unresolved module or unlinked crate `num_cpus`

checkpoint, microvm, remote, remote-tls, serverless, tokio, tui — because only cpu pulled num_cpus, while src/executor/cpu.rs compiles under any feature that enables tokio.

Decided by quorum

Three agy lanes (gemini-3.1-pro-high / 3.8-flash-high / 3.7-flash-high), 3/3 for this option over the three alternatives they were given: gate on all(tokio, num_cpus), add num_cpus to each of the six sibling features, or make it non-optional. Their grounds, which I re-ran rather than took on trust:

  • num_cpus is called in exactly one place in the crate, executor/cpu.rs:41. The six other textual hits are a struct field, a constructor parameter and a getter, all plain usize.
  • std::thread::available_parallelism is stable since Rust 1.59; this workspace's MSRV is 1.91.
  • Deleting the optional dependency removes the defect class, not the instance: there is no longer a dependency for a sibling feature to forget. All three alternatives leave a declaration that can drift again — which is exactly how this got here.

map_or(1, NonZeroUsize::get) preserves the old behaviour exactly: num_cpus::get() returns 1 when it cannot determine the count, and available_parallelism() returns Err in the same situation.

One lane also asserted that num_cpus mishandles cgroup v2 while available_parallelism does not. I have not verified that and it is not load-bearing here — the decision rests on the three grounds above, all of which are checkable from the tree.

Why a separate PR

The quorum suggested landing this inside #3257. That turned out not to be possible: #3257 is in the merge queue, so git push to its branch is refused (GH006), and dequeuing a group whose required checks are passing is not something I'll do to save a rebase. This is independent and safe on main on its own — verified below — so it lands beside #3257 rather than inside it.

Neither PR alone turns the seven green: #3257 removes the tokio half, this removes the num_cpus half. Both are needed.

Verified

On this branch, based on main with #3257 not present:

  • --features cpu and default both check clean
  • aprender-distribute --lib 84/84
  • clippy clean

On #3257's branch with this commit applied — the configuration that matters — all nine acceptance selections (checkpoint microvm remote remote-tls serverless tokio tui cpu full) plus every other feature of the crate check clean, and tests and clippy are clean.

Refs #3262, #3257

no-close: #3262 is the umbrella measurement and stays open until the last two cuda pairs are green; #3257 lands the other half of this crate's fix.

🤖 Generated with Claude Code

… — the 7 selections the tokio gate left red

The `feature = "tokio"` gate in the previous commit is necessary but not
sufficient. Measured on that commit: 17 of 24 single-feature selections go
green and seven stay RED with

    error[E0433]: failed to resolve: use of unresolved module or unlinked crate `num_cpus`

checkpoint, microvm, remote, remote-tls, serverless, tokio, tui — because only
`cpu` pulled `num_cpus` while `src/executor/cpu.rs` compiles under any feature
that enables `tokio`.

DECIDED BY 3-LANE QUORUM (agy, gemini-3.1-pro / 3.8-flash / 3.7-flash), 3/3 for
this option over the three alternatives it was given — gate on `all(tokio,
num_cpus)`, add `num_cpus` to each of the six sibling features, or make it
non-optional. Its grounds, which I re-ran rather than took:

  * `num_cpus` is called in EXACTLY ONE place in the crate,
    `executor/cpu.rs:41`. The six other textual hits are a struct field, a
    constructor parameter and a getter, all plain `usize`.
  * `std::thread::available_parallelism` is stable since Rust 1.59; this
    workspace's MSRV is 1.91.
  * Deleting the optional dependency removes the defect CLASS, not the
    instance: there is no longer a dependency for a sibling feature to forget.
    The three alternatives all leave a declaration that can drift again.

`map_or(1, NonZeroUsize::get)` preserves the old behaviour exactly: `num_cpus::get()`
returns 1 when it cannot determine the count, and `available_parallelism()`
returns `Err` in the same situation.

Verified here, not quoted from the lanes: all 9 acceptance selections plus every
other feature of this crate check clean, default checks clean, clippy is clean,
and aprender-distribute --lib is 84/84.

Refs #3262

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3266 head=9cb567dba8e8fb3539837e0a7ae33e219e89aae8 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

@noahgift
noahgift disabled auto-merge September 14, 2026 17:42
@noahgift
noahgift enabled auto-merge September 14, 2026 18:38
@noahgift
noahgift disabled auto-merge September 14, 2026 22:41
@noahgift noahgift modified the milestones: 0.68.0, 0.70.0 Sep 16, 2026
@noahgift
noahgift added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit e0706bd Sep 16, 2026
18 of 19 checks passed
@noahgift
noahgift deleted the PMAT-1098-numcpus branch September 16, 2026 15:07
noahgift added a commit that referenced this pull request Sep 16, 2026
…roup is gone

Run 35099392561 built gh-readonly-queue/main/pr-3266-2261757fd8a4... from
13:02:23Z to 14:30:59Z -- eighty-eight minutes -- after the queue had already
discarded that entry and rebuilt #3266 on base f5d02de. GitHub does not cancel
the run on a discarded ref: it keeps building, draws runners, and then holds
`gate` queued for fifty minutes on a concurrency group, computing a verdict
nothing can ever read.

THE KEY IS (PR NUMBER, BASE OID), AND BOTH HALVES ARE LOAD-BEARING. A merge
queue ref is `gh-readonly-queue/<base>/pr-<N>-<40 hex>` and the 40 hex IS the
entry's `baseCommit.oid` -- read live from both sides and committed as the
fixture (o1_mq_entries_live.json is the verbatim answer to MQ_QUERY, so the
field names in the predicate cannot drift from the ones GitHub returns).
Matching on the PR number alone answers LEAVE on the one run this rule exists
to cancel -- the zombie and its replacement share the number (row O5). Matching
on the base alone reads one group as licence to judge its neighbour: #3266's
base oid IS #3270's head oid.

NOT REDUNDANT WITH THE DEAD-REF PASS. That pass asks whether the git ref
resolves; this asks whether the GROUP is still an entry of the live queue. They
disagree in both directions -- a discarded group's ref can linger, a live
group's ref can 404 mid-rebuild.

ABSENCE OF EVIDENCE IS NOT ABSENCE OF THE GROUP. Unreadable, wrong-shape, and
EMPTY entries all REFUSE. Empty is the dangerous one: read as "the queue is
empty" it licenses cancelling every merge_group run in flight -- the whole
queue, in one sweep (rows O3/O3b/O3c).

Verified live, against a freshly fetched payload rather than the fixture:
  pr-3270-eb262f8e -> LEAVE  (entry at position 1)
  pr-3266-f5d02deb -> LEAVE  (entry at position 2)
  pr-3266-2261757f -> CANCEL (the measured zombie)
  pr-9999-eb262f8e -> CANCEL (a live base, a PR not in the queue at all)

Mutation proof: disabling the absence check turns O2 red (self-test exit 1,
"got LEAVE, wanted CANCEL"); restored, exit 0. 53 rows, 0 red.

Pmat-Ticket: PMAT-3292
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
guyernest pushed a commit to guyernest/aprender that referenced this pull request Sep 29, 2026
…cancelled; arming refuses --auto unless guard-tree is green on the current head (paiml#3292, paiml#3358) (paiml#3403)

* feat(unwedge): paiml#3292 rule 3 -- cancel a merge_group run whose queue group is gone

Run 35099392561 built gh-readonly-queue/main/pr-3266-2261757fd8a4... from
13:02:23Z to 14:30:59Z -- eighty-eight minutes -- after the queue had already
discarded that entry and rebuilt paiml#3266 on base f5d02de. GitHub does not cancel
the run on a discarded ref: it keeps building, draws runners, and then holds
`gate` queued for fifty minutes on a concurrency group, computing a verdict
nothing can ever read.

THE KEY IS (PR NUMBER, BASE OID), AND BOTH HALVES ARE LOAD-BEARING. A merge
queue ref is `gh-readonly-queue/<base>/pr-<N>-<40 hex>` and the 40 hex IS the
entry's `baseCommit.oid` -- read live from both sides and committed as the
fixture (o1_mq_entries_live.json is the verbatim answer to MQ_QUERY, so the
field names in the predicate cannot drift from the ones GitHub returns).
Matching on the PR number alone answers LEAVE on the one run this rule exists
to cancel -- the zombie and its replacement share the number (row O5). Matching
on the base alone reads one group as licence to judge its neighbour: paiml#3266's
base oid IS paiml#3270's head oid.

NOT REDUNDANT WITH THE DEAD-REF PASS. That pass asks whether the git ref
resolves; this asks whether the GROUP is still an entry of the live queue. They
disagree in both directions -- a discarded group's ref can linger, a live
group's ref can 404 mid-rebuild.

ABSENCE OF EVIDENCE IS NOT ABSENCE OF THE GROUP. Unreadable, wrong-shape, and
EMPTY entries all REFUSE. Empty is the dangerous one: read as "the queue is
empty" it licenses cancelling every merge_group run in flight -- the whole
queue, in one sweep (rows O3/O3b/O3c).

Verified live, against a freshly fetched payload rather than the fixture:
  pr-3270-eb262f8e -> LEAVE  (entry at position 1)
  pr-3266-f5d02deb -> LEAVE  (entry at position 2)
  pr-3266-2261757f -> CANCEL (the measured zombie)
  pr-9999-eb262f8e -> CANCEL (a live base, a PR not in the queue at all)

Mutation proof: disabling the absence check turns O2 red (self-test exit 1,
"got LEAVE, wanted CANCEL"); restored, exit 0. 53 rows, 0 red.

Pmat-Ticket: PMAT-3292
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(arm): refuse --auto unless guard-tree is green on the CURRENT head (paiml#3292)

paiml#3278 was armed on 2026-09-16 while its guard-tree was RED. The merge queue took
it, put it at POSITION 1, and when it failed there it ejected the batch queued
behind it. An arming decision is not local: a PR armed on a red guard costs every
PR behind it a rebuild, and the queue is deepest exactly when that hurts most.

THE STALE-CHECK TRAP IS THE WHOLE POINT. "guard-tree is green on this PR" is a
claim about a SHA, not about a PR, and every name-keyed surface (`gh pr checks`,
`statusCheckRollup`) will show a success from two pushes ago next to the newer
head. So the head sha is read FIRST, the checks come from
repos/{owner}/{repo}/commits/<that sha>/check-runs -- an endpoint that cannot
answer about another commit -- and every row is filtered on `head_sha == <that
sha>` again anyway. A success that exists only on an older sha is the named state
`stale`, not a silent `absent`: the two want different fixes.

ABSENT IS A REFUSAL. No row, no failure, arm -- that is the fail-open shape this
repo keeps shipping. Here it exits 3, as do in_progress, queued, skipped, and a
re-run in flight beside an earlier success on the same sha.

WHY NOT pr_review_quorum_arm.sh. It was searched for first and it IS an arming
path, but it evaluates S13's review-quorum predicate and refuses with Q1 when
there is no signed review receipt, so it cannot be the general arming entry
point; and it reads statusCheckRollup, the surface this trap lives in. The two
compose -- a quorum PERMIT still has to clear this precondition.

12 rows, 0 red, fixtures only, with A1 as the positive control (without it a
function that always refuses satisfies every other row). Verified live in all
three polarities against open PRs, --dry-run:
  paiml#3366 refuse: guard-tree is failure on 179c69b...      exit 3
  paiml#3361 refuse: guard-tree is in_progress on 062e471...  exit 3
  paiml#3363 WOULD-ARM on e5cf234...                          exit 0
  #99999999 (no such PR)                                  exit 2
bashrs: 0 errors.

Pmat-Ticket: PMAT-3292
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The (crate, feature) matrix has never been built: 100 of 430 selections do not compile, all of them unreachable from any default set

1 participant