Skip to content

About

Scoped CPU thread-team placement token (Pool, Exec) for the tensor4all stack

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

threadteam

Splittable CPU placement token for the tensor4all CPU stack. threadteam is the one place that starts tasks: kernels take an Exec token and split it, and no scheduler type appears in any public signature.

Used by strided-rs, tcontract-rs, cpueinsum-rs, tlinalg-rs and tenferro-rs. Design record: docs/design/placement.md.

Usage

use std::sync::atomic::{AtomicUsize, Ordering};
use threadteam::{Exec, Pool};

// The owner builds a pool once and keeps it.
let pool = Pool::builder().threads(4).build()?;

// Per call, the owner mints a token for `budget` slots of the pool.
let mut exec = Exec::new(&pool, 4)?;

// Several phases in sequence: reborrow, because entries consume the token.
let total = AtomicUsize::new(0);
exec.reborrow().for_ranges(1000, 64.try_into().unwrap(), |r| {
    total.fetch_add(r.len(), Ordering::Relaxed);
});

// Independent parts: each job gets a child token with its share of the slots.
exec.fork(2, |_job, child| {
    let _ = child.budget();
});
# Ok::<(), threadteam::Error>(())

Exec::serial() needs no pool and runs everything inline.

Intended consumer pattern

The owner of the CPU resources (an engine, a contraction resource bundle) holds a threadteam::Pool and mints Exec::new(&pool, budget) for each call, passing the token to kernels by value. Kernels never store a token and never mint one.

API

Item Role
Pool, PoolBuilder fixed set of workers; threads, thread_name, stack_size (default 16 MiB, sized for recursive LU), on_start, build
Exec::new(&pool, budget) mint a token; the only clamp (to the pool width); budget == 0 is an error
Exec::serial() the calling thread only; const
Exec::budget, is_serial inspect the token (is_serial means budget 1)
Exec::reborrow(&mut self) same pool and budget while the parent is exclusively borrowed
Exec::limit(max) narrow to min(budget, max)
Exec::fork(jobs, f) min(jobs, budget) tasks; shares differ by at most one and sum to the budget; job i gets a child token
Exec::for_ranges(len, grain, f) at most budget tasks, ranges of at least grain except possibly the last
Exec::broadcast(f) f(i) for i in 0..budget, all concurrent, or Err(Refused)
Exec::can_broadcast() non-consuming: true exactly when broadcast would not refuse right now; ask before building an expensive setup
Exec::carries_team() true when the token has team authority (minted by a Team or a child of one); team() then returns the carried team without acquisition, so a consumer that wants a TeamExec asks this and keeps external for plain tokens
Exec::team(f) / team_with(over, f) reserve budget - 1 awake pool members for a scope and run barrier-bearing sections through TeamExec (run, run_with, split, fork, reborrow), or Err(Refused::Unavailable); see the design record
Pool::into_team(width) -> Team (owns the pool; IntoTeamError gives it back), Team::exec(budget) / try_exec, Team::pool, Team::into_pool persistent reservation for an engine that owns its pool (Pool is not Clone); minted tokens are ordinary Execs whose fork, for_ranges, broadcast, team dispatch on the reserved members; one leader at a time (exec blocks, try_exec -> Refused::Busy); idle members park while reserved
PoolBuilder::qos(Qos), raise_current_thread_qos(Qos) worker QoS on macOS (default UserInitiated); raise the leading thread without ever lowering it
PoolBuilder::idle_spin, idle_runs_rayon, acquire_timeout; Pool::release_idle waiting policy of idle team members; a reserved member spins with one PAUSE per poll (never sched_yield), parks after the delay, and parks after a short spin in a root scope that has more participants than the caller's CPUs
PoolBuilder::pin_participants(bool), Pool::pin_participants() opt-in placement of the members of a root team scope (Linux; off by default, which is consistent with the rule it protects): member k (participant k >= 1) pins itself onto the k-th CPU of the calling thread's affinity set, S[0] is left to the leader, and a member restores its own mask before it returns to Rayon or serves foreign work. The leader is never pinned, because a vendor library creates its thread team from the calling thread and a one-CPU caller would confine the whole provider team; a member that calls such a library directly inside a share does confine it, so use Exec::external (which restores the member's mask first) or leave pinning off. For tensor4all/faer#107; give each team its own CPU set (taskset -c), there is no CPU allocator
DispatchCosts, WidthCosts, PoolBuilder::dispatch_costs, Exec::dispatch_cost(width), TeamExec::dispatch_cost(width) cost of one parallel section per kind of token (team, in pool, outside), steady state; one row per HostClass (HostClass::detect(), keyed by kernel dispatch: x86_64-avx512, x86_64-avx2, x86_64-scalar, aarch64-apple, aarch64, generic), DispatchCosts::for_class(class); measured rows for x86_64-avx512 (team anchors at widths 2, 4, 6 and 8) and aarch64-apple, a measured Zen2 team row for x86_64-avx2 (anchors at ten widths from 2 to 64; its plain kinds stay generic), every other class takes the conservative generic row (3 / 15 / 40 us at the anchors 2, 4, 6); a row holds measured anchors (WidthCosts::new, WidthCosts::flat, or WidthCosts::from_anchors for measured anchors up to 64 and beyond), interpolates between them and grows linearly above the largest one, so a width above the anchors is never charged the cost of the largest anchor; DispatchCosts::host_default() is the detected class's row; Duration::MAX when serial; override per pool
pays_at(est, sections, cost), Exec::pays(est, sections, width), Exec::width_for(est, sections), width_for_by, PAYS the one rule: parallel iff est >= PAYS * sections * D(width); width_for minimises sections * D(k) + est / k
Exec::external(f) enter the pool once and hand the budget as a plain width to an outside library (faer, a vendor BLAS); how it divides the width is that library's concern

Contracts

  • No duplication. Exec is neither Clone nor Copy (compile-fail doctests), and every parallel entry consumes it.
  • Slots are conserved. Under one root token the number of tasks running at once is at most its budget at every nesting depth. A parent waiting for its children keeps no slot.
  • Lazy and inline. A serial token, budget one, or one task runs inline on the calling thread and makes no scheduler call, so the process-global Rayon pool is never started implicitly.
  • Placement by caller. A worker of the target pool spawns into the current scope without installing; a plain thread or a worker of another pool enters the target pool once and waits. No thread-local state exists in the crate.
  • broadcast refuses with Refused::ForkChild for a token derived from fork (through reborrow or limit too), and with Refused::CallerInPool when the caller is a worker of the same pool. Roots, and their reborrows and limits, are eligible. Callers must handle Refused by falling back to a barrier-free path. Concurrent broadcasts on one pool queue in the same order on every worker (no interleaving); participants are the pool's workers 0..budget, so blocking work of another token on those workers delays the broadcast. can_broadcast applies the same rule through one private helper, so the two cannot disagree.
  • Deterministic shutdown. Dropping a Pool joins every worker thread: when drop returns, all workers have exited and their thread-local destructors (including state created in an on_start hook) have run. A pool dropped from one of its own workers cannot join itself, so that drop only signals shutdown and returns without joining. Without parallel there is nothing to join.
  • No scheduler type is public. There is no constructor from, and no accessor returning, a scheduler pool. scripts/check-no-public-rayon.sh runs cargo public-api -p threadteam (when installed) and fails if rayon:: appears in the output.

Features

Feature Effect
parallel (default) run on a private pool. Without it the same types exist, PoolBuilder::build returns a width-1 pool and every entry runs inline
ledger test support: ledger::max_live(root), live, installs, and Exec::ledger_root. Records live tasks per root token and pool installs; compiled out when off

Consumers assert ledger::max_live(root) <= budget in their own tests, and keep a test binary that runs serial and pooled work and then checks that rayon::ThreadPoolBuilder::new().build_global() still succeeds.

License

Licensed under either of MIT or Apache-2.0 at your option.

About

Scoped CPU thread-team placement token (Pool, Exec) for the tensor4all stack

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages