strided_exec/exec.rs
1//! The splittable placement token.
2
3use crate::error::{Error, Refused};
4use crate::pool::Pool;
5use crate::sched;
6use crate::split;
7use crate::track::Track;
8use core::fmt;
9use core::num::NonZeroUsize;
10use core::ops::Range;
11use core::sync::atomic::{AtomicUsize, Ordering};
12
13/// `budget` slots of a [`Pool`], or the calling thread alone.
14///
15/// An `Exec` is the only way kernels start parallel work. It is a *token*:
16///
17/// * It is neither `Clone` nor `Copy`, and every parallel entry consumes it, so
18/// a kernel cannot run two phases at full width at the same time. Use
19/// [`reborrow`](Exec::reborrow) to run several phases in sequence.
20/// * Only [`Exec::new`] mints a token, and it needs the `&Pool`; a token never
21/// hands its pool back out, so a kernel cannot mint a fresh full-budget token.
22/// * [`fork`](Exec::fork) divides the budget among its tasks (shares differ by
23/// at most one and sum to the budget), so the number of tasks running at once
24/// under one root token is at most its budget at every nesting depth.
25///
26/// A parent waiting for its children keeps no slot.
27///
28/// # Placement
29///
30/// Serial, budget one, or one task: the work runs inline on the calling thread
31/// and the scheduler is never called, so no global pool is started. The caller
32/// is a worker of the pool: the tasks run as a join tree on the caller, nothing is
33/// installed. The caller is a plain thread or a worker of a different pool: the
34/// pool is entered once and the caller waits; the tasks run on the target pool.
35/// No thread-local state is involved at any point.
36///
37/// # Examples
38///
39/// ```
40/// use std::sync::atomic::{AtomicUsize, Ordering};
41/// use strided_exec::{Exec, Pool};
42///
43/// let pool = Pool::builder().threads(2).build()?;
44/// let exec = Exec::new(&pool, 2)?;
45/// let sum = AtomicUsize::new(0);
46/// exec.for_ranges(100, 1.try_into().unwrap(), |r| {
47/// sum.fetch_add(r.sum::<usize>(), Ordering::Relaxed);
48/// });
49/// assert_eq!(sum.into_inner(), 4950);
50/// # Ok::<(), strided_exec::Error>(())
51/// ```
52///
53/// A token cannot be duplicated:
54///
55/// ```compile_fail
56/// fn needs_clone<T: Clone>() {}
57/// needs_clone::<strided_exec::Exec<'static>>();
58/// ```
59///
60/// ```compile_fail
61/// fn needs_copy<T: Copy>() {}
62/// needs_copy::<strided_exec::Exec<'static>>();
63/// ```
64///
65/// ```compile_fail
66/// let exec = strided_exec::Exec::serial();
67/// exec.fork(1, |_, _| {});
68/// exec.fork(1, |_, _| {}); // `fork` consumed the token
69/// ```
70#[must_use = "an Exec is a budget of execution slots; pass it to a kernel"]
71pub struct Exec<'p> {
72 pool: Option<&'p Pool>,
73 budget: NonZeroUsize,
74 /// Produced by `fork` (directly or through `reborrow`).
75 child: bool,
76 track: Track,
77}
78
79impl Exec<'static> {
80 /// A token that runs everything inline on the calling thread.
81 ///
82 /// It never touches a scheduler, so using it cannot start any pool.
83 ///
84 /// # Examples
85 ///
86 /// ```
87 /// use strided_exec::Exec;
88 ///
89 /// let exec = Exec::serial();
90 /// assert!(exec.is_serial());
91 /// let here = std::thread::current().id();
92 /// exec.fork(3, |_, _| assert_eq!(std::thread::current().id(), here));
93 /// ```
94 pub const fn serial() -> Exec<'static> {
95 Exec {
96 pool: None,
97 budget: NonZeroUsize::MIN,
98 child: false,
99 track: Track::NONE,
100 }
101 }
102}
103
104impl<'p> Exec<'p> {
105 /// A token of `budget` slots of `pool`, clamped to the pool's width.
106 ///
107 /// This is the only place a budget is clamped.
108 ///
109 /// # Errors
110 ///
111 /// [`Error::ZeroBudget`] when `budget` is zero.
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use strided_exec::{Error, Exec, Pool};
117 ///
118 /// let pool = Pool::builder().threads(2).build()?;
119 /// let exec = Exec::new(&pool, 64)?;
120 /// assert_eq!(exec.budget(), pool.width());
121 /// assert_eq!(Exec::new(&pool, 0).unwrap_err(), Error::ZeroBudget);
122 /// # Ok::<(), Error>(())
123 /// ```
124 pub fn new(pool: &'p Pool, budget: usize) -> Result<Exec<'p>, Error> {
125 let budget = NonZeroUsize::new(budget).ok_or(Error::ZeroBudget)?;
126 Ok(Exec {
127 pool: Some(pool),
128 budget: budget.min(pool.width()),
129 child: false,
130 track: Track::root(),
131 })
132 }
133
134 /// The number of slots this token stands for.
135 ///
136 /// # Examples
137 ///
138 /// ```
139 /// assert_eq!(strided_exec::Exec::serial().budget().get(), 1);
140 /// ```
141 pub fn budget(&self) -> NonZeroUsize {
142 self.budget
143 }
144
145 /// Whether no parallelism is possible: the budget is one slot.
146 ///
147 /// Kernels use this to take their serial path directly.
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// assert!(strided_exec::Exec::serial().is_serial());
153 /// ```
154 pub fn is_serial(&self) -> bool {
155 self.budget.get() == 1
156 }
157
158 /// Narrows the token to at most `max` slots.
159 ///
160 /// The result has budget `min(budget, max)` and the same pool; the dropped
161 /// slots are simply not used, so slots are conserved. Callers narrow before
162 /// [`broadcast`](Exec::broadcast) or [`external`](Exec::external) to match a team size chosen
163 /// by their own work model. Co-scheduling eligibility is unchanged by
164 /// `limit`: a root token stays eligible for `broadcast`, a child produced by
165 /// [`fork`](Exec::fork) stays ineligible.
166 ///
167 /// # Examples
168 ///
169 /// ```
170 /// use std::num::NonZeroUsize;
171 /// use strided_exec::{Exec, Pool};
172 ///
173 /// let pool = Pool::builder().threads(4).build()?;
174 /// let exec = Exec::new(&pool, 4)?.limit(NonZeroUsize::new(2).unwrap());
175 /// assert!(exec.budget().get() <= 2);
176 /// # Ok::<(), strided_exec::Error>(())
177 /// ```
178 pub fn limit(self, max: NonZeroUsize) -> Exec<'p> {
179 Exec {
180 budget: self.budget.min(max),
181 ..self
182 }
183 }
184
185 /// A token with the same pool and budget, borrowed from `self`.
186 ///
187 /// Parallel entries consume the token, but a kernel often runs several
188 /// parallel phases in sequence. Each phase takes a reborrow. The parent is
189 /// exclusively borrowed and unusable while the child lives, so the slots are
190 /// held by one token at a time: this conserves slots exactly as handing the
191 /// token over would.
192 ///
193 /// # Examples
194 ///
195 /// ```
196 /// use strided_exec::Exec;
197 ///
198 /// let mut exec = Exec::serial();
199 /// exec.reborrow().fork(2, |_, _| {});
200 /// exec.reborrow().fork(2, |_, _| {});
201 /// exec.fork(2, |_, _| {});
202 /// ```
203 pub fn reborrow(&mut self) -> Exec<'_> {
204 Exec {
205 pool: self.pool,
206 budget: self.budget,
207 child: self.child,
208 track: self.track.alias(),
209 }
210 }
211
212 /// Runs `jobs` jobs on `min(jobs, budget)` tasks; job `i` gets a child token.
213 ///
214 /// The budget is split into one share per task, shares differing by at most
215 /// one and summing to the budget. A job's child token carries its task's
216 /// share and may be passed to any kernel, which can `fork` again inside it.
217 /// When `jobs` exceeds the budget the tasks take job indices from a shared
218 /// counter, so the load balances; otherwise job `i` runs on task `i`.
219 ///
220 /// One task, a serial token, or `jobs <= 1` runs inline on the calling
221 /// thread without any scheduler call. Returns when every job has finished.
222 ///
223 /// # Examples
224 ///
225 /// ```
226 /// use std::sync::atomic::{AtomicUsize, Ordering};
227 /// use strided_exec::{Exec, Pool};
228 ///
229 /// let pool = Pool::builder().threads(2).build()?;
230 /// let hits: Vec<AtomicUsize> = (0..5).map(|_| AtomicUsize::new(0)).collect();
231 /// Exec::new(&pool, 2)?.fork(5, |i, _child| {
232 /// hits[i].fetch_add(1, Ordering::Relaxed);
233 /// });
234 /// assert!(hits.iter().all(|h| h.load(Ordering::Relaxed) == 1));
235 /// # Ok::<(), strided_exec::Error>(())
236 /// ```
237 pub fn fork<F>(self, jobs: usize, f: F)
238 where
239 F: Fn(usize, Exec<'_>) + Sync,
240 {
241 if jobs == 0 {
242 return;
243 }
244 let budget = self.budget.get();
245 let tasks = jobs.min(budget);
246 let _released = self.track.release();
247 let next = AtomicUsize::new(0);
248 let this = &self;
249 let body = |k: usize| {
250 let _live = this.track.task();
251 let share = split::share(budget, tasks, k);
252 let child = || Exec {
253 pool: this.pool,
254 // `share >= 1` because `tasks <= budget`.
255 budget: NonZeroUsize::new(share).unwrap_or(NonZeroUsize::MIN),
256 child: true,
257 track: this.track.child(),
258 };
259 if jobs <= tasks {
260 f(k, child());
261 } else {
262 loop {
263 let job = next.fetch_add(1, Ordering::Relaxed);
264 if job >= jobs {
265 break;
266 }
267 f(job, child());
268 }
269 }
270 };
271 match self.pool {
272 Some(pool) if tasks > 1 => sched::run_tasks(pool, tasks, &self.track, body),
273 _ => {
274 for k in 0..tasks {
275 body(k);
276 }
277 }
278 }
279 }
280
281 /// Data-parallel loop over `0..len` in contiguous ranges.
282 ///
283 /// Uses at most `budget` tasks; every range is at least `grain` long except
284 /// possibly the last, and together the ranges cover `0..len` exactly once.
285 /// When one task suffices (`len <= grain`, a serial token) `f(0..len)` runs
286 /// inline on the calling thread without any scheduler call. `len == 0` does
287 /// nothing.
288 ///
289 /// # Examples
290 ///
291 /// ```
292 /// use std::sync::atomic::{AtomicUsize, Ordering};
293 /// use strided_exec::{Exec, Pool};
294 ///
295 /// let pool = Pool::builder().threads(2).build()?;
296 /// let covered = AtomicUsize::new(0);
297 /// Exec::new(&pool, 2)?.for_ranges(10, 4.try_into().unwrap(), |r| {
298 /// covered.fetch_add(r.len(), Ordering::Relaxed);
299 /// });
300 /// assert_eq!(covered.into_inner(), 10);
301 /// # Ok::<(), strided_exec::Error>(())
302 /// ```
303 pub fn for_ranges<F>(self, len: usize, grain: NonZeroUsize, f: F)
304 where
305 F: Fn(Range<usize>) + Sync,
306 {
307 if len == 0 {
308 return;
309 }
310 let (chunk, tasks) = split::range_plan(len, grain.get(), self.budget.get());
311 let _released = self.track.release();
312 let track = &self.track;
313 let body = |k: usize| {
314 let _live = track.task();
315 f(split::chunk_range(len, chunk, k));
316 };
317 match self.pool {
318 Some(pool) if tasks > 1 => sched::run_tasks(pool, tasks, track, body),
319 _ => {
320 for k in 0..tasks {
321 body(k);
322 }
323 }
324 }
325 }
326
327 /// Co-scheduling for barrier-bearing kernels: runs `f(i)` for every
328 /// `i in 0..budget` with all participants running concurrently, or refuses.
329 ///
330 /// A serial or budget-one token runs `f(0)` inline and returns `Ok`.
331 /// Otherwise the pool is entered once and every participant runs on its own
332 /// worker, so `f` may block on a barrier shared by the `budget`
333 /// participants.
334 ///
335 /// # Errors
336 ///
337 /// Nothing runs and [`Refused`] is returned when co-scheduling cannot be
338 /// guaranteed:
339 ///
340 /// * [`Refused::ForkChild`]: the token was produced by [`fork`](Exec::fork)
341 /// (or derived from such a token by `reborrow` or `limit`); its slots may
342 /// share workers with siblings. Roots from [`Exec::new`], and their
343 /// `reborrow`s and `limit`s, stay eligible.
344 ///
345 /// Callers must handle `Refused` by falling back to a barrier-free path.
346 /// * [`Refused::CallerInPool`]: the calling thread is a worker of the
347 /// token's pool and would occupy a slot.
348 ///
349 /// # Guarantee and limits
350 ///
351 /// Concurrent broadcasts on one pool are queued on every worker in the same
352 /// order, so two of them cannot each hold half the workers. The participants
353 /// are the pool's workers `0..budget`; if unrelated tasks that never finish
354 /// occupy those workers, the broadcast waits for them. Keep barrier kernels on a pool whose workers are not held
355 /// by blocking work of another token.
356 ///
357 /// # Examples
358 ///
359 /// ```
360 /// use std::sync::Barrier;
361 /// use strided_exec::{Exec, Pool};
362 ///
363 /// let pool = Pool::builder().threads(2).build()?;
364 /// let exec = Exec::new(&pool, 2)?;
365 /// let barrier = Barrier::new(exec.budget().get());
366 /// exec.broadcast(|_i| {
367 /// barrier.wait(); // every participant is running, or this call refused
368 /// }).expect("called from a plain thread on a root token");
369 /// # Ok::<(), strided_exec::Error>(())
370 /// ```
371 pub fn broadcast<F>(self, f: F) -> Result<(), Refused>
372 where
373 F: Fn(usize) + Sync,
374 {
375 let pool = match self.pool {
376 Some(pool) if self.budget.get() > 1 => pool,
377 _ => {
378 let _live = self.track.task();
379 f(0);
380 return Ok(());
381 }
382 };
383 if self.child {
384 return Err(Refused::ForkChild);
385 }
386 if sched::in_pool(pool) {
387 return Err(Refused::CallerInPool);
388 }
389 let _released = self.track.release();
390 #[cfg(feature = "parallel")]
391 sched::broadcast(pool, &self.track, self.budget.get(), f);
392 #[cfg(not(feature = "parallel"))]
393 {
394 let _ = pool;
395 f(0);
396 }
397 Ok(())
398 }
399
400 /// Hands the budget as a plain width to a library outside this stack.
401 ///
402 /// This is the only place a bare width exists. The token is consumed, the
403 /// pool is entered once (inline when the token is serial, has a budget of
404 /// one, or the caller already is a worker of the pool) and `f` receives the
405 /// budget, for example to build `faer::Par::rayon(width)` or to set a vendor
406 /// BLAS thread count. Whatever the library schedules with that width runs on
407 /// this pool, never on an ambient one. How the library divides the width is
408 /// its own responsibility. Without the `parallel` feature, `f` runs inline
409 /// and receives a width of one.
410 ///
411 /// # Examples
412 ///
413 /// ```
414 /// use strided_exec::Exec;
415 ///
416 /// let width = Exec::serial().external(|width| width.get());
417 /// assert_eq!(width, 1);
418 /// ```
419 pub fn external<R: Send>(self, f: impl FnOnce(NonZeroUsize) -> R + Send) -> R {
420 #[cfg(feature = "parallel")]
421 if let Some(pool) = self.pool {
422 if self.budget.get() > 1 {
423 let width = self.budget;
424 let _released = self.track.release();
425 return sched::enter(pool, &self.track, move || f(width));
426 }
427 }
428 f(NonZeroUsize::MIN)
429 }
430
431 /// Root id of this token in the [`ledger`](crate::ledger), if it has one.
432 ///
433 /// Tokens from [`Exec::new`] are roots; children and reborrows report the
434 /// root of their origin; [`Exec::serial`] has none.
435 ///
436 /// Available with the `ledger` cargo feature.
437 ///
438 /// # Examples
439 ///
440 /// ```
441 /// use strided_exec::{Exec, Pool};
442 ///
443 /// let pool = Pool::builder().threads(1).build()?;
444 /// assert!(Exec::new(&pool, 1)?.ledger_root().is_some());
445 /// assert!(Exec::serial().ledger_root().is_none());
446 /// # Ok::<(), strided_exec::Error>(())
447 /// ```
448 #[cfg(feature = "ledger")]
449 pub fn ledger_root(&self) -> Option<u64> {
450 self.track.root_id()
451 }
452}
453
454impl fmt::Debug for Exec<'_> {
455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 f.debug_struct("Exec")
457 .field("budget", &self.budget)
458 .field("pooled", &self.pool.is_some())
459 .field("child", &self.child)
460 .finish()
461 }
462}