@@ -23,8 +23,6 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
2323use std:: sync:: { Mutex , Once , OnceLock } ;
2424use std:: thread:: Thread ;
2525
26- use system_info:: NUM_THREADS ;
27-
2826/// Idle spins before a worker parks: long enough to stay hot across back-to-back dispatches,
2927/// short enough to yield the core during sequential gaps.
3028const SPIN_LIMIT : u32 = 1 << 12 ;
@@ -33,18 +31,18 @@ const SPIN_LIMIT: u32 = 1 << 12;
3331/// million-task kernels to a few thousand claims.
3432const MAX_CLAIM_BATCH : usize = 1 << 12 ;
3533
36- /// Worker count including the dispatcher (= build-time `NUM_THREADS` ).
34+ /// Worker count including the dispatcher. Resolved once at runtime (see [`system_info::num_threads`] ).
3735#[ must_use]
38- pub const fn num_threads ( ) -> usize {
39- NUM_THREADS
36+ pub fn num_threads ( ) -> usize {
37+ system_info :: num_threads ( )
4038}
4139
4240/// Chunk size for a flat fan-out: a few chunks per worker — fine enough for the counter to
4341/// rebalance heterogeneous cores, coarse enough to amortize dispatch.
4442#[ must_use]
4543#[ inline]
4644pub fn recommended_chunk_size ( n_items : usize ) -> usize {
47- n_items. div_ceil ( NUM_THREADS * 4 ) . max ( 1 )
45+ n_items. div_ceil ( num_threads ( ) * 4 ) . max ( 1 )
4846}
4947
5048thread_local ! {
@@ -107,28 +105,20 @@ unsafe impl Send for Pool {}
107105
108106/// Idempotent warm-up: spawn workers and run one empty dispatch so the pool and the (macOS)
109107/// lazily-allocated mutex exist before timed work; otherwise the pool inits on first use.
110- ///
111- /// Also fail-fast if the machine's core count differs from the build-time [`NUM_THREADS`] (which
112- /// sizes the pool): a mismatch silently over/under-subscribes every kernel.
113108pub fn init ( ) {
114109 static INIT : Once = Once :: new ( ) ;
115110 INIT . call_once ( || {
116- let actual = std:: thread:: available_parallelism ( ) . unwrap ( ) . get ( ) ;
117- assert_eq ! (
118- actual, NUM_THREADS ,
119- "parallel pool built for {NUM_THREADS} threads but this machine reports {actual} -> please rebuild with env variable: LEANVM_NUM_THREADS={actual}"
120- ) ;
121111 let _ = pool ( ) ;
122- if NUM_THREADS > 1 {
123- for_each_index ( NUM_THREADS , |_| { } ) ;
112+ if num_threads ( ) > 1 {
113+ for_each_index ( num_threads ( ) , |_| { } ) ;
124114 }
125115 } ) ;
126116}
127117
128118fn pool ( ) -> & ' static Pool {
129119 static POOL : OnceLock < & ' static Pool > = OnceLock :: new ( ) ;
130120 POOL . get_or_init ( || {
131- let n = NUM_THREADS . max ( 1 ) ;
121+ let n = num_threads ( ) . max ( 1 ) ;
132122 let p: & ' static Pool = Box :: leak ( Box :: new ( Pool {
133123 job : UnsafeCell :: new ( None ) ,
134124 generation : AtomicUsize :: new ( 0 ) ,
@@ -200,6 +190,7 @@ fn drain(pool: &Pool) {
200190 // SAFETY: `job.f` borrows a `&dyn Fn` the blocked dispatcher keeps live.
201191 let f = unsafe { job. f . as_ref ( ) } ;
202192 let n = job. n_tasks ;
193+ let nt = num_threads ( ) ;
203194 let prev = IN_TASK . replace ( true ) ; // catch nested dispatch (see `for_each_chunk`)
204195 // Catch a task panic so it can't unwind across `worker_main` (skipping the `working`
205196 // decrement → deadlock) or poison the dispatch lock; `for_each_chunk` re-raises it.
@@ -210,7 +201,7 @@ fn drain(pool: &Pool) {
210201 if observed >= n {
211202 break ;
212203 }
213- let batch = ( ( n - observed) / ( NUM_THREADS * 2 ) ) . clamp ( 1 , MAX_CLAIM_BATCH ) ;
204+ let batch = ( ( n - observed) / ( nt * 2 ) ) . clamp ( 1 , MAX_CLAIM_BATCH ) ;
214205 let start = pool. counter . fetch_add ( batch, Ordering :: Relaxed ) ;
215206 if start >= n {
216207 break ;
@@ -232,7 +223,8 @@ pub fn for_each_chunk<F: Fn(usize, usize) + Sync>(n_tasks: usize, f: F) {
232223 assert ! ( !IN_TASK . get( ) , "nested parallel dispatch from within a pool task" ) ;
233224
234225 // Trivial sizes / single-core builds run inline.
235- if NUM_THREADS <= 1 || n_tasks <= 1 {
226+ let nt = num_threads ( ) ;
227+ if nt <= 1 || n_tasks <= 1 {
236228 if n_tasks > 0 {
237229 f ( 0 , n_tasks) ;
238230 }
@@ -252,7 +244,7 @@ pub fn for_each_chunk<F: Fn(usize, usize) + Sync>(n_tasks: usize, f: F) {
252244 // SAFETY: sole writer — prior dispatch fully drained (`working == 0`), next not yet observed.
253245 unsafe { * pool. job . get ( ) = Some ( Job { f : f_erased, n_tasks } ) } ;
254246 pool. counter . store ( 0 , Ordering :: Relaxed ) ;
255- pool. working . store ( NUM_THREADS - 1 , Ordering :: Release ) ;
247+ pool. working . store ( nt - 1 , Ordering :: Release ) ;
256248 pool. generation . fetch_add ( 1 , Ordering :: SeqCst ) ; // publish; SeqCst guards the park protocol
257249
258250 // Wake only parked workers; spinning ones see the bump for free.
@@ -389,7 +381,7 @@ pub fn par_fill<T: Send, F: Fn(usize) -> T + Sync>(dst: &mut [T], build: F) {
389381/// `run(slot, start, end)` fires once per claimed batch with that worker's slot, so state
390382/// accumulates across its batches. Returns the slots (rest `None`) for the caller to combine.
391383fn drain_into_slots < S : Send > ( n_tasks : usize , run : impl Fn ( & mut Option < S > , usize , usize ) + Sync ) -> Vec < Option < S > > {
392- let mut slots: Vec < Option < S > > = ( 0 ..NUM_THREADS ) . map ( |_| None ) . collect ( ) ;
384+ let mut slots: Vec < Option < S > > = ( 0 ..num_threads ( ) ) . map ( |_| None ) . collect ( ) ;
393385 let ptr = SendPtr ( slots. as_mut_ptr ( ) ) ;
394386 for_each_chunk ( n_tasks, |start, end| {
395387 // SAFETY: `current_worker_id() < NUM_THREADS` is unique per live worker → disjoint
0 commit comments