From a189ceb5ac0798c0eace9deccd635e98937868cb Mon Sep 17 00:00:00 2001 From: Hiroshi Shinaoka Date: Thu, 29 Oct 2026 20:08:13 +0900 Subject: [PATCH] team: with_team docs count participants, say it is for one-off calls, and show an owned Pool and Team (#149) Co-Authored-By: Claude Opus 5.5 --- README.md | 27 +++++++++++++++--- faer/src/lib.rs | 8 +++++- faer/src/utils/team.rs | 63 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f7c9dc6e..5074fae3 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,34 @@ There is no process wide parallelism setting. Every operation that can run in pa ```rust faer::team::with_team(8, |par| { - // every faer call in here with `par` runs its regions on the eight reserved members + // every faer call in here with `par` runs its regions on 8 participants: the calling thread + // and 7 reserved workers qr_in_place(a.as_mut(), h.as_mut(), par, stack, params); }); ``` A caller that already owns a team lends a sub-team to each call with `par.reborrow()`; a width of one -is `Par::Seq`. `with_team` reserves a team for the duration of the closure, so a caller that keeps a -budget across many calls should own a team itself. See the *parallelism* section of the crate -documentation for the full picture. +is `Par::Seq`. + +`with_team` is for a one-off call: every call builds a new pool, so it spawns its worker threads and +joins them again when the closure ends. Code that runs parallel work repeatedly should own the +`threadteam` `Pool` and `Team` itself, build them once, and lend a `Par` from the team to each call: + +```rust +// once: 8 participants, the calling thread and 7 reserved workers +let pool = threadteam::Pool::builder().threads(8).build()?; +let team = pool.into_team(8).ok().expect("an idle pool"); + +// per call: lend the team to faer for the duration of the closure +for (a, h) in work { + let mut exec = team.exec(8); + exec.team(|t| qr_in_place(a.as_mut(), h.as_mut(), Par::Team(t), stack, params)) + .ok() + .expect("a team token for an idle team"); +} +``` + +See the *parallelism* section of the crate documentation for the full picture. # minimum supported rust version diff --git a/faer/src/lib.rs b/faer/src/lib.rs index 57378722..df833f67 100644 --- a/faer/src/lib.rs +++ b/faer/src/lib.rs @@ -203,7 +203,8 @@ //! Default::default(), //! )); //! faer::team::with_team(4, |par| { -//! // every faer call in here with `par` runs its regions on the four reserved members +//! // every faer call in here with `par` runs its regions on 4 participants: the calling +//! // thread and 3 reserved workers //! faer::linalg::svd::svd( //! a.as_ref(), //! s.as_mut().col_mut(0).as_diagonal_mut(), @@ -220,6 +221,11 @@ //! the members busy-wait for a bounded time between regions, then sleep; the width is the one the //! caller asked `with_team` for, and there is no process-wide setting to override it. //! +//! `with_team` is for a one-off call: each call builds a new pool, spawning its worker threads and +//! joining them again at the end. Code that runs parallel work repeatedly should build a +//! [`threadteam::Pool`] and [`threadteam::Team`] once and lend a `Par` from the team to each call +//! (see the example on [`team::with_team`]). +//! //! # crate features //! //! - `std`: enabled by default. links with the standard library to enable diff --git a/faer/src/utils/team.rs b/faer/src/utils/team.rs index 94b8adea..f3034e8e 100644 --- a/faer/src/utils/team.rs +++ b/faer/src/utils/team.rs @@ -29,15 +29,49 @@ use threadteam::TeamExec; /// let a = Mat::::from_fn(8, 8, |i, j| (i + j) as f64); /// let mut dst = Mat::::zeros(8, 8); /// faer::team::with_team(2, |par| { -/// // every faer call in here runs its regions on the two reserved members +/// // every faer call in here runs its regions on 2 participants: the calling thread and +/// // 1 reserved worker /// faer::linalg::matmul::matmul(dst.as_mut(), Accum::Replace, a.as_ref(), a.as_ref(), 1.0, par); /// }); /// ``` /// /// `width` is clamped to at least one; a width of one is [`crate::Par::Seq`] and reserves nothing. -/// The pool is the host's own ([`threadteam::Pool::builder`], `width` threads); a caller that -/// already owns a pool or a team should borrow from it directly instead, because two pools do not -/// share their members. +/// The team has `width` participants: the calling thread and `width - 1` reserved workers. The pool +/// is the host's own ([`threadteam::Pool::builder`], `width` threads); a caller that already owns a +/// pool or a team should borrow from it directly instead, because two pools do not share their +/// members. +/// +/// `with_team` is for a one-off call: every call builds a new pool, so it spawns its worker threads +/// and joins them again when the closure ends. A caller that runs parallel work repeatedly should +/// own the [`threadteam::Pool`] and [`threadteam::Team`] itself, build them once, and lend a `Par` +/// from the team to each call: +/// +/// ``` +/// use faer::{Accum, Mat, Par}; +/// let a = Mat::::from_fn(8, 8, |i, j| (i + j) as f64); +/// let mut dst = Mat::::zeros(8, 8); +/// +/// // once: 4 participants, the calling thread and 3 reserved workers +/// let pool = threadteam::Pool::builder().threads(4).build().unwrap(); +/// let team = pool.into_team(4).ok().expect("an idle pool"); +/// +/// // per call: lend the team to faer for the duration of the closure +/// for _ in 0..3 { +/// let mut exec = team.exec(4); +/// exec.team(|t| { +/// faer::linalg::matmul::matmul( +/// dst.as_mut(), +/// Accum::Replace, +/// a.as_ref(), +/// a.as_ref(), +/// 1.0, +/// Par::Team(t), +/// ); +/// }) +/// .ok() +/// .expect("a team token for an idle team"); +/// } +/// ``` /// /// Panics if the team cannot be built or acquired: the closure's members wake asynchronously, so /// the acquisition waits as long as the team tests do (30 s) instead of threadteam's 2 ms default, @@ -726,6 +760,27 @@ mod tests { exec.team(|team| f(Par::Team(team))).ok().expect("a team token") } + /// `with_team(width)` hands out `width` participants: the calling thread and `width - 1` + /// reserved workers. `into_team` clamps the width to the pool's, so a pool of `width - 1` + /// threads would give one participant fewer. + #[test] + fn with_team_has_width_participants() { + for width in [2usize, 3, 4] { + let size = with_team(width, |par| match par { + Par::Team(t) => t.size().get(), + _ => 1, + }); + assert!(size == width); + } + let pool = Pool::builder() + .threads(3) + .acquire_timeout(std::time::Duration::from_secs(30)) + .build() + .unwrap(); + let team = pool.into_team(4).ok().expect("an idle pool"); + assert!(team.size().get() == 3); + } + fn rand_mat(m: usize, n: usize, seed: u64) -> Mat { let mut x = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); Mat::from_fn(m, n, |_, _| {