From 82982f9c356e6ece5a3395379190601412c56e99 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 14 Sep 2026 14:37:52 +0200 Subject: [PATCH] =?UTF-8?q?fix(distribute):=20drop=20num=5Fcpus=20for=20st?= =?UTF-8?q?d::thread::available=5Fparallelism=20=E2=80=94=20the=207=20sele?= =?UTF-8?q?ctions=20the=20tokio=20gate=20left=20red?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Cargo.lock | 1 - crates/aprender-distribute/Cargo.toml | 3 +-- crates/aprender-distribute/src/executor/cpu.rs | 9 ++++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17f0f58887..2dc6f08477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -714,7 +714,6 @@ dependencies = [ "criterion 0.5.1", "crossterm 0.28.1", "futures", - "num_cpus", "parquet", "pepita", "pollster", diff --git a/crates/aprender-distribute/Cargo.toml b/crates/aprender-distribute/Cargo.toml index d7fee9105b..4d41b871d6 100644 --- a/crates/aprender-distribute/Cargo.toml +++ b/crates/aprender-distribute/Cargo.toml @@ -14,7 +14,7 @@ documentation = "https://docs.rs/repartir" [features] default = ["cpu"] -cpu = ["tokio", "num_cpus"] +cpu = ["tokio"] gpu = ["wgpu", "pollster"] remote = ["tokio", "bincode", "tracing-subscriber"] remote-tls = ["remote", "rustls", "tokio-rustls", "rustls-pemfile"] @@ -34,7 +34,6 @@ name = "repartir" tokio = { version = "1.35", features = ["rt-multi-thread", "sync", "time", "process", "io-util", "macros", "net"], optional = true } # CPU backend -num_cpus = { version = "1.16", optional = true } # GPU backend (pure Rust via gfx-rs) # wgpu provides cross-platform GPU compute (Vulkan/Metal/DX12/WebGPU) diff --git a/crates/aprender-distribute/src/executor/cpu.rs b/crates/aprender-distribute/src/executor/cpu.rs index 4ba21a3864..548880a6fc 100644 --- a/crates/aprender-distribute/src/executor/cpu.rs +++ b/crates/aprender-distribute/src/executor/cpu.rs @@ -36,9 +36,16 @@ impl CpuExecutor { /// Creates a new CPU executor. /// /// Detects the number of available CPU cores automatically. + /// + /// Uses `std::thread::available_parallelism`, stable since Rust 1.59 and far + /// below this workspace's 1.91 MSRV, rather than the `num_cpus` crate. That + /// dependency was optional and pulled by ONE feature (`cpu`) while this file + /// compiles under any feature enabling `tokio`, so six sibling selections + /// could not link it. Falling back to 1 when the count is undeterminable + /// matches what `num_cpus::get()` returned in the same situation. #[must_use] pub fn new() -> Self { - let num_cpus = num_cpus::get(); + let num_cpus = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get); info!("CpuExecutor initialized with {num_cpus} cores"); Self { num_cpus } }