Threads¶
By default navis-fastcore uses every core it can see. That is the right answer
for one call in one process, and the wrong one when you are already spreading
work over processes — because nothing tells a worker process that it is one of
twenty, so every worker claims the whole machine.
# 20 worker processes on a 224-core node, each building a 224-thread pool:
# 4480 threads over 224 cores.
navis.heal_skeleton(nl, parallel=True, n_cores=20)
Measured on exactly that node, healing 40 skeletons of 200k nodes each, this was slower than running the same work on a single worker (6.71 s vs 5.10 s) while burning 2.3x the CPU. Capping each worker to one thread brought it to 3.60 s at a sixth of the CPU.
Setting it¶
set_num_threads sizes the pool for the whole
process. Call it once, before any other fastcore function:
Under navis' multiprocessing, set it in each worker as it starts. The hook is
shipped to the workers by pickle, so it has to be picklable — a
functools.partial of this function is, a lambda is not:
import functools
import navis
import navis_fastcore as fastcore
navis.compute.worker_init_hooks.append(
functools.partial(fastcore.set_num_threads, 1)
)
navis.heal_skeleton(nl, parallel=True, n_cores=20)
RAYON_NUM_THREADS does the same thing without a code change, and is inherited by
spawned workers if you set it in the parent before the pool starts:
What "once per process" means¶
The pool is built at most once, by whichever of these comes first: an earlier
set_num_threads, RAYON_NUM_THREADS, a call to
get_num_threads, or simply the first parallel
fastcore call. After that it cannot be resized: calling set_num_threads again
with the same value is a no-op — which is what makes it safe in a worker-init
hook that fires more than once — and with a different value raises
RuntimeError.
Note that get_num_threads builds the pool as a side effect, so asking before
setting is what makes the set fail. Set first, ask second.
Per-call caps¶
Most functions also take a threads (or n_cores) argument, which caps that call
alone:
This builds a fresh thread pool for the duration of the call, so it costs n
thread spawns every time — fine for one big all-by-all NBLAST, wasteful for a
small call in a loop. Prefer set_num_threads when the cap is a property of the
process rather than of the call.
Is this your problem?¶
scripts/profile-heal-parallel.py in the repository measures it, separating
oversubscription from the two things that look identical from the outside:
serialising neurons to the workers, and healing simply not having enough parallel
work in it to fill the cores it claims.
API¶
navis_fastcore.set_num_threads(n)
¶
Set the number of threads used for parallel work in this process.
By default fastcore uses every core it can see, which is the right answer for a single call in a single process and the wrong one when the caller is already spreading work over processes. Nothing tells a worker process that it is one of twenty, so it has to be told.
Call this once, before any other fastcore function.
| PARAMETER | DESCRIPTION |
|---|---|
n
|
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
RuntimeError
|
If the thread pool already exists at a different size. It is
built once per process by whichever comes first: an earlier
|
See Also
:func:get_num_threads
Read the current thread count back.
Examples:
Under navis' multiprocessing, set it in each worker as it starts. The hook is
shipped to the workers by pickle, so it has to be picklable — a
functools.partial of this function is, a lambda is not:
>>> import navis, functools
>>> navis.compute.worker_init_hooks.append(
... functools.partial(fastcore.set_num_threads, 1)
... )
>>> navis.heal_skeleton(nl, parallel=True, n_cores=20)
Calling it repeatedly with the same value is fine, which is what makes it safe in a hook that fires more than once. Calling it with a different value raises, because the pool cannot be resized once built.
navis_fastcore.get_num_threads()
¶
Number of threads available for parallel work in this process.
Note that asking builds the thread pool if it does not exist yet — which is
then exactly what makes a subsequent :func:set_num_threads fail. Set first,
ask second.
| RETURNS | DESCRIPTION |
|---|---|
int
|
|
See Also
:func:set_num_threads
Set the thread count.