strided-exec
Splittable CPU placement for the strided stack. strided-exec is the one place
that starts tasks: kernels take an Exec token and split it, and no scheduler
type appears in any public signature.
Design record: docs/design/cpu-exec-placement.md (placement proposal for
tenferro-rs#2206, #2272, #2273).
Usage
use ;
use ;
// The owner builds a pool once and keeps it.
let pool = builder.threads.build?;
// Per call, the owner mints a token for `budget` slots of the pool.
let mut exec = new?;
// Several phases in sequence: reborrow, because entries consume the token.
let total = new;
exec.reborrow.for_ranges;
// Independent parts: each job gets a child token with its share of the slots.
exec.fork;
# Ok::
Exec::serial() needs no pool and runs everything inline.
Intended consumer pattern
The owner of the CPU resources (an engine, tcontract-exec's pool) holds a
strided_exec::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.
Today tcontract-exec's Exec is Copy and its Pool::rayon_pool() is public;
both go away when tcontract migrates to this crate.
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::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.
Execis neitherClonenorCopy(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.
broadcastrefuses withRefused::ForkChildfor a token derived fromfork(throughreborroworlimittoo), and withRefused::CallerInPoolwhen the caller is a worker of the same pool. Roots, and theirreborrows andlimits, are eligible. Callers must handleRefusedby 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 workers0..budget, so blocking work of another token on those workers delays the broadcast.- No scheduler type is public. There is no constructor from, and no accessor
returning, a scheduler pool.
scripts/check-no-public-rayon.shrunscargo public-api -p strided-exec(when installed) and fails ifrayon::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.