Backends¶
Where the counting happens. See Running in parallel for the guide.
Selecting one¶
thresher.backends.get_backend ¶
get_backend(backend: Any) -> Backend
Resolve the backend option to something that can do the counting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend
|
Any
|
the name of a backend, or an object already implementing the protocol -
which is how a caller passes a pre-configured |
required |
Returns:
| Type | Description |
|---|---|
Backend
|
The backend to use. |
Raises:
| Type | Description |
|---|---|
UnknownBackendError
|
if the name is not recognised. It is a |
BackendDependencyError
|
if |
Note
The names build default instances. To configure one - MultiprocessingBackend(
num_workers=4), RayBackend(num_shards=...) - construct it and pass the object
as the backend option instead of the name.
Source code in src/thresher/backends/__init__.py
The contract¶
thresher.backends.base ¶
The execution-backend contract, and the pure map/reduce steps behind it.
A backend decides where the counting happens, never what the answer is. Every backend must return bit-identical results for the same input; only the distribution of the work changes. That is why the map and reduce steps live here as plain functions rather than inside any one backend - they are shared verbatim, and can be tested without a cluster.
Two primitives cover every algorithm that can be parallelised:
tally_candidates
Score a fixed list of candidate thresholds against the data. Linear search and grid
search are both "score these candidates, keep the best", so both reduce to this.
class_counts_by_score
Count the classes at each distinct score. The exact sweep needs only these counts, not
the samples themselves, so the per-record work distributes and the driver is left with
one pass over the distinct scores.
Backend ¶
Bases: Protocol
Where the counting happens.
Implementations must not change the answer - see the module docstring.
tally_candidates ¶
tally_candidates(
candidates: Sequence[float],
scores: Sequence[float],
actual_classes: Sequence[int],
) -> list[int]
Count correct predictions for each candidate threshold, over all the data.
class_counts_by_score ¶
class_counts_by_score(
scores: Sequence[float], actual_classes: Sequence[int]
) -> dict[float, ClassCounts]
Count negatives and positives at each distinct score, over all the data.
tally_chunk ¶
tally_chunk(
candidates: Sequence[float],
scores: Sequence[float],
actual_classes: Sequence[int],
) -> list[int]
Count correct predictions per candidate, for one shard of the data.
This is the map step of tally_candidates, and it is deliberately a free function:
the local backend calls it directly and the Ray backend ships it to workers, so both
run exactly the same code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
Sequence[float]
|
the thresholds to score. |
required |
scores
|
Sequence[float]
|
this shard's scores. |
required |
actual_classes
|
Sequence[int]
|
this shard's classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
list[int]
|
One count per candidate, in the same order: how many of this shard's samples |
list[int]
|
that candidate classifies correctly. |
Source code in src/thresher/backends/base.py
merge_tallies ¶
Add per-shard tallies together elementwise.
This is the reduce step of tally_candidates. Addition is associative and
commutative, so the order shards arrive in cannot affect the result - which is what
lets the answer be identical across backends.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
partials
|
Iterable[Sequence[int]]
|
one tally list per shard, all the same length. |
required |
Returns:
| Type | Description |
|---|---|
list[int]
|
The summed tallies. |
Raises:
| Type | Description |
|---|---|
ShardMergeError
|
if no partials were given, or they disagree on length. It is a
|
Source code in src/thresher/backends/base.py
count_chunk ¶
Count negatives and positives at each distinct score, for one shard.
The map step of class_counts_by_score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
this shard's scores. |
required |
actual_classes
|
Sequence[int]
|
this shard's classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
dict[float, ClassCounts]
|
A mapping of score to |
Source code in src/thresher/backends/base.py
merge_counts ¶
Merge per-shard score counts by summing them.
The reduce step of class_counts_by_score, and order-independent for the same reason
merge_tallies is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
partials
|
Iterable[Mapping[float, ClassCounts]]
|
one score-to-counts mapping per shard. |
required |
Returns:
| Type | Description |
|---|---|
dict[float, ClassCounts]
|
The combined mapping. |
Source code in src/thresher/backends/base.py
plan_shards ¶
Work out the shard boundaries for a dataset.
Kept separate from any backend so the arithmetic can be tested on its own, including on machines where Ray cannot be installed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
total
|
int
|
number of samples. |
required |
workers
|
int
|
how many shards are wanted at most, normally the cluster's CPU count. |
required |
min_rows
|
int
|
smallest worthwhile shard. Below this the coordination costs more than the work saved, so fewer, larger shards are produced instead. |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
A list of |
list[tuple[int, int]]
|
order and without gaps. Empty when |
Source code in src/thresher/backends/base.py
Local¶
thresher.backends.local.LocalBackend ¶
Do the work here, in this process.
This is what every version before 0.4.2 did, and what still happens unless a different backend is asked for.
tally_candidates ¶
tally_candidates(
candidates: Sequence[float],
scores: Sequence[float],
actual_classes: Sequence[int],
) -> list[int]
Count correct predictions for each candidate threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
Sequence[float]
|
the thresholds to score. |
required |
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
list[int]
|
One count per candidate, in the same order. |
Source code in src/thresher/backends/local.py
class_counts_by_score ¶
class_counts_by_score(
scores: Sequence[float], actual_classes: Sequence[int]
) -> dict[float, ClassCounts]
Count negatives and positives at each distinct score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
dict[float, ClassCounts]
|
A mapping of score to |
Source code in src/thresher/backends/local.py
Multiprocessing¶
thresher.backends.mp_backend.MultiprocessingBackend ¶
MultiprocessingBackend(
num_workers: int | None = None,
min_rows_per_shard: int = DEFAULT_MIN_ROWS_PER_SHARD,
)
Count in parallel across this machine's CPU cores.
Example
from thresher import Thresher Thresher(backend="mp").optimize_threshold(scores, actual_classes) # doctest: +SKIP
Note
Because the workers are separate processes, any script that builds one of these at
module level must sit behind an if __name__ == "__main__": guard. Without it the
workers re-import the script and build their own pools; see the module docstring.
That mistake is reported rather than left to hang.
Configure how the data is divided, and over how many processes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_workers
|
int | None
|
how many worker processes to use. Defaults to one per processor;
|
None
|
min_rows_per_shard
|
int
|
do not produce shards smaller than this. Below it the work is done in this process instead, since a fork would cost more than it saves. |
DEFAULT_MIN_ROWS_PER_SHARD
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
if |
Source code in src/thresher/backends/mp_backend.py
tally_candidates ¶
tally_candidates(
candidates: Sequence[float],
scores: Sequence[float],
actual_classes: Sequence[int],
) -> list[int]
Count correct predictions per candidate, sharded across processes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
Sequence[float]
|
the thresholds to score. |
required |
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
list[int]
|
One count per candidate, identical to what the local backend returns. |
Raises:
| Type | Description |
|---|---|
ParallelBootstrapError
|
if the worker processes could not start, which on a
re-importing start method means a missing |
Source code in src/thresher/backends/mp_backend.py
class_counts_by_score ¶
class_counts_by_score(
scores: Sequence[float], actual_classes: Sequence[int]
) -> dict[float, ClassCounts]
Count classes per distinct score, sharded across processes.
This is the step that makes the exact sweep parallel: each worker returns one count per distinct score it saw, never the samples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
dict[float, ClassCounts]
|
A mapping of score to |
Raises:
| Type | Description |
|---|---|
ParallelBootstrapError
|
if the worker processes could not start. |
Source code in src/thresher/backends/mp_backend.py
thresher.backends.mp_backend.resolve_worker_count ¶
Turn a requested worker count into a usable number of processes.
Shared with linear search's n_jobs, which names the same quantity, so the two cannot
disagree about what -1 means or about which values are refusable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_workers
|
int | None
|
how many processes to ask for. |
required |
Returns:
| Type | Description |
|---|---|
int
|
A process count of at least 1, never more than the machine has processors. |
int
|
Over-asking is clamped rather than refused: how many cores are available is a |
int
|
property of the machine, not a mistake in the caller's code. |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
for 0, or anything below -1, which name no sensible number of
processes. It is a |
Source code in src/thresher/backends/mp_backend.py
Ray¶
thresher.backends.ray_backend.RayBackend ¶
Count in parallel across a Ray cluster.
Connects to whatever cluster Ray is already attached to. If Ray has not been
initialised, it is started locally with default settings - so a caller who has already
called ray.init(address=...) keeps their cluster, and one who has not gets a working
local cluster without ceremony.
Configure how the data is divided.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_shards
|
int | None
|
how many shards to split into. Defaults to the cluster's CPU count, which is the useful maximum since each shard occupies one worker. |
None
|
min_rows_per_shard
|
int
|
do not produce shards smaller than this. Sharding a small dataset costs more in scheduling than it saves in computation. |
DEFAULT_MIN_ROWS_PER_SHARD
|
Raises:
| Type | Description |
|---|---|
BackendDependencyError
|
if Ray is not installed, an |
Source code in src/thresher/backends/ray_backend.py
tally_candidates ¶
tally_candidates(
candidates: Sequence[float],
scores: Sequence[float],
actual_classes: Sequence[int],
) -> list[int]
Count correct predictions per candidate, sharded across the cluster.
The candidate list is put into the object store once and shared by reference, so it is not re-serialised per shard.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
candidates
|
Sequence[float]
|
the thresholds to score. |
required |
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
list[int]
|
One count per candidate, identical to what the local backend returns. |
Source code in src/thresher/backends/ray_backend.py
class_counts_by_score ¶
class_counts_by_score(
scores: Sequence[float], actual_classes: Sequence[int]
) -> dict[float, ClassCounts]
Count classes per distinct score, sharded across the cluster.
This is the step that makes the exact sweep distributable: the driver never sees the samples, only one count per distinct score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scores
|
Sequence[float]
|
the values being split. |
required |
actual_classes
|
Sequence[int]
|
the matching classes, as -1 and 1. |
required |
Returns:
| Type | Description |
|---|---|
dict[float, ClassCounts]
|
A mapping of score to |