Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "threadteam"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
license = "MIT OR Apache-2.0"
authors = ["Satoshi Terasaki", "Hiroshi Shinaoka"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ the token to kernels by value. Kernels never store a token and never mint one.
| `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 `Exec`s 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 |
| `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`, 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 |
| `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 |

Expand Down
10 changes: 6 additions & 4 deletions docs/design/placement.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,13 +584,15 @@ serial_estimate >= PAYS * sections * D(width) (PAYS = 3)
machine takes, not by the CPU model: `x86_64-avx512` (runtime `avx512f`), `x86_64-avx2` (`avx2` + `fma`),
`x86_64-scalar`, `aarch64-apple` (aarch64 on macOS), `aarch64`, `generic`; detected once per process.
`x86_64-avx512` holds the measured Ryzen row (team 0.6/0.7/0.9 at widths 2/4/6 and the measured 0.98 at width 8,
in pool 3.0/3.6/3.5, outside 9.7/10.1/8.7 us), `aarch64-apple` the measured Apple M5 row at widths 2, 4 and 6, and
every class without a measurement the conservative generic row (3, 15, 40 microseconds at the anchors; the
in pool 3.0/3.6/3.5, outside 9.7/10.1/8.7 us), `aarch64-apple` the measured Apple M5 row at widths 2, 4 and 6,
`x86_64-avx2` a measured Zen2 team row at ten widths from 2 to 64 (1.42 to 9.33 us; in pool and outside generic;
thresholds section 3.7), and every class without a measurement the conservative generic row (3, 15, 40 microseconds at the anchors; the
extrapolation charges 32, 160 and 427 microseconds at width 64). A consumer that measured its own host at more
widths overrides the row with `WidthCosts::from_anchors` (up to `WidthCosts::MAX_ANCHORS` = 12 anchors). The rates
of the consumer crates (strided-rs, tcontract-rs, faer) are
tables of their own, keyed by the same class names, and a class's rates and dispatch costs come from the same
machine in the same clock state: boost on/off moves both by about 1.66x and leaves the ratio stable within a
tables of their own, keyed by the same class names, and a measured class's rates and dispatch costs come from the
same machine in the same clock state (the exception is the `x86_64-avx2` team row, whose consumer rates are still
generic; thresholds section 3.7): boost on/off moves both by about 1.66x and leaves the ratio stable within a
few percent (`faer-benchmark` `raw-exec3-boost`, `tables-*-exec3-boost.md`). Each crate carries an identical
`HostClass` and `detect()`, kept in sync with `threadteam`'s by hand.
`PoolBuilder::dispatch_costs` overrides it.
Expand Down
141 changes: 141 additions & 0 deletions docs/design/thresholds-data/raw/broccoli-dispatch-20261029.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
kind,width,ns
team/hot,2,1333.7
team/gap1ms,2,1740.0
team/gap10ms,2,2410.0
team/gap300ms,2,10450.0
team/load10us-minus-10us,2,1423.0
team/fork-hot,2,1659.6
team/hot,4,2111.0
team/gap1ms,4,2490.0
team/gap10ms,4,3050.0
team/gap300ms,4,16680.0
team/load10us-minus-10us,4,1991.6
team/fork-hot,4,2258.6
team/hot,6,2525.1
team/gap1ms,6,2820.0
team/gap10ms,6,3420.0
team/gap300ms,6,21030.0
team/load10us-minus-10us,6,2328.6
team/fork-hot,6,2655.1
team/hot,8,2950.1
team/gap1ms,8,3250.0
team/gap10ms,8,3890.0
team/gap300ms,8,23771.0
team/load10us-minus-10us,8,2737.7
team/fork-hot,8,3277.8
team/hot,12,3931.7
team/gap1ms,12,3980.0
team/gap10ms,12,4710.0
team/gap300ms,12,31351.0
team/load10us-minus-10us,12,3557.5
team/fork-hot,12,4162.5
team/hot,16,4581.1
team/gap1ms,16,4710.0
team/gap10ms,16,5460.0
team/gap300ms,16,41570.0
team/load10us-minus-10us,16,4076.0
team/fork-hot,16,5161.3
team/hot,24,4619.9
team/gap1ms,24,4640.0
team/gap10ms,24,5550.0
team/gap300ms,24,58701.0
team/load10us-minus-10us,24,4292.1
team/fork-hot,24,5467.6
team/hot,32,5044.2
team/gap1ms,32,4750.0
team/gap10ms,32,5700.0
team/gap300ms,32,81231.0
team/load10us-minus-10us,32,4837.8
team/fork-hot,32,5679.8
team/hot,48,6041.5
team/gap1ms,48,5740.0
team/gap10ms,48,6220.0
team/gap300ms,48,107962.0
team/load10us-minus-10us,48,6401.9
team/fork-hot,48,6396.0
team/hot,64,10334.5
team/gap1ms,64,7670.0
team/gap10ms,64,8620658.0
team/gap300ms,64,143273.0
team/load10us-minus-10us,64,9332.5
team/fork-hot,64,8112.1
plain/outside-hot,2,14398.6
plain/outside-gap1ms,2,21711.0
plain/outside-gap10ms,2,46121.0
plain/outside-load10us-minus-10us,2,42975.1
external/outside-hot,2,10932.7
external/outside-gap1ms,2,20711.0
plain/in-pool,2,816.3
plain/in-pool-load10us-minus-10us,2,27529.1
plain/outside-hot,4,21425.4
plain/outside-gap1ms,4,24051.0
plain/outside-gap10ms,4,49971.0
plain/outside-load10us-minus-10us,4,70437.9
external/outside-hot,4,10103.6
external/outside-gap1ms,4,19851.0
plain/in-pool,4,2761.1
plain/in-pool-load10us-minus-10us,4,54263.4
plain/outside-hot,6,28269.5
plain/outside-gap1ms,6,26930.0
plain/outside-gap10ms,6,54001.0
plain/outside-load10us-minus-10us,6,83079.7
external/outside-hot,6,12181.5
external/outside-gap1ms,6,20200.0
plain/in-pool,6,5834.0
plain/in-pool-load10us-minus-10us,6,72908.3
plain/outside-hot,8,32576.6
plain/outside-gap1ms,8,71521.0
plain/outside-gap10ms,8,59012.0
plain/outside-load10us-minus-10us,8,76791.7
external/outside-hot,8,9062.7
external/outside-gap1ms,8,19560.0
plain/in-pool,8,9396.2
plain/in-pool-load10us-minus-10us,8,67186.3
plain/outside-hot,12,39899.4
plain/outside-gap1ms,12,74381.0
plain/outside-gap10ms,12,275665.0
plain/outside-load10us-minus-10us,12,96968.4
external/outside-hot,12,9433.1
external/outside-gap1ms,12,21260.0
plain/in-pool,12,16428.7
plain/in-pool-load10us-minus-10us,12,84907.8
plain/outside-hot,16,45699.4
plain/outside-gap1ms,16,146703.0
plain/outside-gap10ms,16,75732.0
plain/outside-load10us-minus-10us,16,91376.0
external/outside-hot,16,12133.7
external/outside-gap1ms,16,18720.0
plain/in-pool,16,23073.7
plain/in-pool-load10us-minus-10us,16,77622.0
plain/outside-hot,24,56698.6
plain/outside-gap1ms,24,146253.0
plain/outside-gap10ms,24,395218.0
plain/outside-load10us-minus-10us,24,100128.0
external/outside-hot,24,11789.4
external/outside-gap1ms,24,20230.0
plain/in-pool,24,36303.3
plain/in-pool-load10us-minus-10us,24,90913.5
plain/outside-hot,32,63561.6
plain/outside-gap1ms,32,266016.0
plain/outside-gap10ms,32,386408.0
plain/outside-load10us-minus-10us,32,83287.2
external/outside-hot,32,8614.3
external/outside-gap1ms,32,21001.0
plain/in-pool,32,42314.6
plain/in-pool-load10us-minus-10us,32,82865.7
plain/outside-hot,48,75502.5
plain/outside-gap1ms,48,268636.0
plain/outside-gap10ms,48,396378.0
plain/outside-load10us-minus-10us,48,95110.1
external/outside-hot,48,10625.6
external/outside-gap1ms,48,20660.0
plain/in-pool,48,54374.3
plain/in-pool-load10us-minus-10us,48,96198.8
plain/outside-hot,64,76841.3
plain/outside-gap1ms,64,323237.0
plain/outside-gap10ms,64,390518.0
plain/outside-load10us-minus-10us,64,99850.3
external/outside-hot,64,9728.1
external/outside-gap1ms,64,20520.0
plain/in-pool,64,58670.7
plain/in-pool-load10us-minus-10us,64,93638.2
42 changes: 40 additions & 2 deletions docs/design/thresholds.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ and `from_anchors` accepts measured anchors up to `u16::MAX`, so a row measured
extrapolation where it has data. Above the last anchor the cost is never flat: a wide section is charged at least
in proportion to its width, so `width_for` no longer answers the full width just because the largest anchor pays (a
flat 1 us row gives 1, 2, 4 and 8 us at widths 6, 12, 24 and 48; a 24 us region on such a row selects width 12 of a
budget 64). Section 3.6 keeps the historical reason for the earlier flat model. A host that is not measured gets the
budget 64). Section 3.6 keeps the historical reason for the earlier flat model; section 3.7 is the `x86_64-avx2` team
row with anchors to width 64. A host that is not measured gets the
flat generic row (team 3, in pool 15, outside 40 microseconds at the anchors 2, 4 and 6, and the width proportion
above), which errs towards staying serial.

Expand Down Expand Up @@ -548,7 +549,7 @@ there: width 8 is 4.67 us in the pool and 11.6 us from outside.
| plain in pool | 3.0 | 3.6 | 3.5 | |
| plain outside | 9.7 | 10.1 | 8.7 | |

The row is `DispatchCosts::for_class(HostClass::X86_64Avx512)` in `cost.rs`, and `host_default()` returns it on a host whose runtime detection finds `avx512f` (the other x86_64 classes, `x86_64-avx2` and `x86_64-scalar`, take the generic row until measured). The row and the rates of the consumer crates for the same class come from this machine at 2.0 GHz (boost off); boost on moves both by about 1.66x with a stable ratio (`raw-exec3-boost`, `tables-*-exec3-boost.md`):
The row is `DispatchCosts::for_class(HostClass::X86_64Avx512)` in `cost.rs`, and `host_default()` returns it on a host whose runtime detection finds `avx512f` (`x86_64-avx2` has its own team row since section 3.7; `x86_64-scalar` takes the generic row until measured). The row and the rates of the consumer crates for the same class come from this machine at 2.0 GHz (boost off); boost on moves both by about 1.66x with a stable ratio (`raw-exec3-boost`, `tables-*-exec3-boost.md`):

```rust
HostClass::X86_64Avx512 => DispatchCosts {
Expand Down Expand Up @@ -609,6 +610,43 @@ needs the ledger and `Participant::size` to exclude the empty shares to keep "a
than k tasks of real work". the `x86_64-avx512` row now holds the post-fix row (0.6, 0.7, 0.9), re-measured by the Linux
campaign.

### 3.7 Zen2 team row (x86_64-avx2, threadteam-rs#6)

`for_class(HostClass::X86_64Avx2)` carries a measured team row with ten anchors. Host `broccoli`: AMD EPYC 7702P
(Zen2, 64 physical cores, SMT off, NUMA 1, 16 MiB L3 per 4-core CCX), `cpufreq/boost = 0`, governor `performance`,
nothing else running. The Linux port of this crate's `benches/dispatch_cost.rs` that takes the pool size and the
widths from the environment (`faer-benchmark` branch `bench/awake-team-linux`,
`data/probes/20261026-awake-team-linux/bench/tt/src/bin/dispatch_cost.rs`, unchanged; the checked-in bench here
does not read these variables) built against threadteam 0.1.0, `TT_POOL=64`, `TT_WIDTHS=2,4,6,8,12,16,24,32,48,64`,
process pinned to CPUs 0-63, 2026-10-29. The row is
`team/load10us-minus-10us`, the same quantity as the other rows (back to back sections whose shares carry 10 us of
work, minus the 10 us), rounded to the nearest nanosecond. Raw output: `thresholds-data/raw/broccoli-dispatch-20261029.csv`.

| width | 2 | 4 | 6 | 8 | 12 | 16 | 24 | 32 | 48 | 64 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| team, us | 1.42 | 1.99 | 2.33 | 2.74 | 3.56 | 4.08 | 4.29 | 4.84 | 6.40 | 9.33 |

The cost grows more slowly than the width between 6 and 64 (2.33 to 9.33 us for a width ratio of 10.7). Widths
between the ten anchors interpolate, so a host of up to 64 cores never reaches the extrapolation; above 64 the
linear extrapolation is the chosen conservative policy, not a measured bound.

What the row does not claim:

* **Mixed provenance.** For `x86_64-avx512` and `aarch64-apple` the dispatch row and the consumer crates' rates of
the class come from one machine in one clock state. The consumer crates (faer, tcontract-rs, strided-rs) still
hold their generic rates for `x86_64-avx2`, so on this class the gate compares a measured dispatch cost with a
conservative rate. It is a heuristic exception, recorded here, until those rates are measured on the same host.
* **One class, many hosts.** `x86_64-avx2` covers every AVX2 + FMA host, small desktops included. The width 2 team
floor moves from the generic 9 us (`PAYS * 3 us`) to 4.3 us. The row is a representative default for the class, not
a guarantee for a given topology; a pool owner with its own measurement overrides it with
`PoolBuilder::dispatch_costs`.
* **Plain kinds unchanged.** In pool and outside deliberately keep the generic row (15 and 40 us at the anchors). The
run has plain-token numbers too (in the CSV), but they are a control the measurement was not designed for and are
not promoted to the row.
* **Fresh team entry.** tensor4all/faer#91 saw, on the same host, an intermittent section of about 0.62 ms when a
caller enters a fresh `Team::exec(w).team(...)` per call; the anchor instrument reuses one token and never shows it.
Its cause is not attributed. The row is the steady-state cost of a reused token and does not include it.

## 4. Limits of this data

* One host, rough, 6 threads only (widths 2 and 4 only for the dispatch cost), one process per mode
Expand Down
Loading
Loading