sc_parallel

sc_parallel

Functions to allow parallelization to be performed easily.

NB: Uses multiprocess instead of multiprocessing under the hood for broadest support across platforms (e.g. Jupyter notebooks).

Highlights

Classes

Name Description
Parallel Parallelization manager

Parallel

sc_parallel.Parallel(
    func,
    iterarg=None,
    iterkwargs=None,
    args=None,
    kwargs=None,
    ncpus=None,
    maxcpu=None,
    maxmem=None,
    interval=None,
    parallelizer=None,
    serial=False,
    progress=False,
    callback=None,
    globaldict=None,
    label=None,
    capture=False,
    die=True,
    lbkwargs=None,
    **func_kwargs,
)

Parallelization manager

For arguments and usage documentation, see sc.parallelize(). Briefly, this class validates input arguments, sets the number of CPUs, creates a process (or thread) pool, starts the jobs running, retrieves the results from each job, and processes them into outputs.

Useful methods

reset(): reset the Parallel object to its initial pre-run state run_async(): the method that actually executes the parallelization (NB, used with every method, not only async ones) monitor(): monitor the progress of an asynchronous run finalize(): get the results from each job and process it run(): shortcut to calling run_async() followed by finalize()

Useful attributes and properties

running (bool): whether or not the jobs are running ready (bool): whether or not the jobs are ready status (str): a string description of the current state (not run, running, or done) jobs (list): a list of jobs to run or being run (empty prior to run) results (list): list of all results (the output from the jobs; empty prior to run) success (list): whether each job completed successfully (true/false) exceptions (list): if not, store the exceptions that were raised times (dict): timing information on when the jobs were started, when they finished, and how long each job took

Example:

import sciris as sc

def slowfunc(i):
    sc.randsleep(seed=i)
    return i**2

# Standard usage
P = sc.Parallel(slowfunc, iterarg=range(10), parallelizer='multiprocess-async')
P.run_async()
P.monitor()
P.finalize()
print(P.times)
  • New in version 3.0.0.
  • New in version 3.1.0: “globaldict” argument

Methods

Name Description
disp Display the full representation of the object
finalize Get results from the jobs and close the pool
init Perform all remaining initialization steps; this can safely be called after object creation
make_argslist Construct argument list
make_pool Make the pool and map function
monitor Monitor progress – only usable with async
process_results Parse the returned results dict into separate lists
reset Reset to the pre-run state
run Actually run the parallelization
run_async Choose how to run in parallel, and do it
set_defaults Define defaults for parallelization
set_method Choose which method to use for parallelization
set_ncpus Configure number of CPUs
validate_args Validate iterarg and iterkwargs
disp
sc_parallel.Parallel.disp()

Display the full representation of the object

finalize
sc_parallel.Parallel.finalize(
    get_results=True,
    close_pool=True,
    process_results=True,
)

Get results from the jobs and close the pool

init
sc_parallel.Parallel.init()

Perform all remaining initialization steps; this can safely be called after object creation

make_argslist
sc_parallel.Parallel.make_argslist()

Construct argument list

make_pool
sc_parallel.Parallel.make_pool()

Make the pool and map function

monitor
sc_parallel.Parallel.monitor(interval=0.1, **kwargs)

Monitor progress – only usable with async

process_results
sc_parallel.Parallel.process_results()

Parse the returned results dict into separate lists

reset
sc_parallel.Parallel.reset()

Reset to the pre-run state

run
sc_parallel.Parallel.run()

Actually run the parallelization

run_async
sc_parallel.Parallel.run_async()

Choose how to run in parallel, and do it

set_defaults
sc_parallel.Parallel.set_defaults()

Define defaults for parallelization

set_method
sc_parallel.Parallel.set_method()

Choose which method to use for parallelization

set_ncpus
sc_parallel.Parallel.set_ncpus()

Configure number of CPUs

validate_args
sc_parallel.Parallel.validate_args()

Validate iterarg and iterkwargs

Functions

Name Description
cpu_count Alias to multiprocessing.cpu_count()
cpuload Takes a snapshot of current CPU usage via psutil
loadbalancer Delay execution while CPU load is too high – a very simple load balancer.
memload Takes a snapshot of current fraction of memory usage via psutil
parallelize Execute a function in parallel.

cpu_count

sc_parallel.cpu_count()

Alias to multiprocessing.cpu_count()

cpuload

sc_parallel.cpuload(interval=0.1)

Takes a snapshot of current CPU usage via psutil

Parameters

Name Type Description Default
interval float number of seconds over which to estimate CPU load 0.1

Returns

Name Type Description
a float between 0-1 representing the fraction of psutil.cpu_percent() currently used.

loadbalancer

sc_parallel.loadbalancer(
    maxcpu=0.9,
    maxmem=0.9,
    index=None,
    interval=None,
    cpu_interval=0.1,
    maxtime=36000,
    label=None,
    verbose=None,
    **kwargs,
)

Delay execution while CPU load is too high – a very simple load balancer.

Parameters

Name Type Description Default
maxcpu (float) the maximum CPU load to allow for the task to still start required
maxmem (float) the maximum memory usage to allow for the task to still start required
index (int) the index of the task – used to start processes asynchronously (default None) required
interval (float) the time delay to poll to see if CPU load is OK (default 0.5 seconds) required
cpu_interval (float) number of seconds over which to estimate CPU load (default 0.1; too small gives inaccurate readings) required
maxtime (float) maximum amount of time to wait to start the task (default 36000 seconds (10 hours)) required
label (str) the label to print out when outputting information about task delay or start (default None) required
verbose (bool) whether or not to print information about task delay or start (default None, which shows if a task is delayed) required

Examples:

# Simplest usage -- delay if CPU or memory load is >80%
sc.loadbalancer()

# Use a maximum CPU load of 50%, maximum memory of 90%, and stagger the start by process number
for nproc in processlist:
    sc.loadbalancer(maxload=0.5, maxmem=0.8, index=nproc)
  • New in version 2.0.0: maxmem argument; maxload renamed maxcpu
  • New in version 3.0.0: maxcpu and maxmem set to 0.9 by default
  • New in version 3.2.5: moved from sc_profiling to sc_parallel

memload

sc_parallel.memload()

Takes a snapshot of current fraction of memory usage via psutil

Note on the different functions:

- `sc.memload()` checks current total system memory consumption
- `sc.checkram()` checks RAM (virtual memory) used by the current Python process
- `sc.checkmem()` checks memory consumption by a given object

Returns

Name Type Description
a float between 0-1 representing the fraction of psutil.virtual_memory() currently used.

parallelize

sc_parallel.parallelize(
    func,
    iterarg=None,
    iterkwargs=None,
    args=None,
    kwargs=None,
    ncpus=None,
    maxcpu=None,
    maxmem=None,
    interval=None,
    parallelizer=None,
    serial=False,
    progress=False,
    callback=None,
    globaldict=None,
    capture=False,
    die=True,
    lbkwargs=None,
    **func_kwargs,
)

Execute a function in parallel.

Most simply, sc.parallelize() acts as a shortcut for using pool.map. However, it also provides flexibility in how arguments are passed to the function, load balancing, etc.

Either or both of iterarg or iterkwargs can be used. iterarg can be an iterable or an integer; if the latter, it will run the function that number of times and not pass the argument to the function (which may be useful for running “embarrassingly parallel” simulations). iterkwargs is a dict of iterables; each iterable must be the same length (and the same length of iterarg, if it exists), and each dict key will be used as a kwarg to the called function. Any other kwargs passed to sc.parallelize() will also be passed to the function.

This function can either use a fixed number of CPUs or allocate dynamically based on load. If ncpus is None, then it will allocate the number of CPUs dynamically. Memory (maxmem) and CPU load (maxcpu) limits can also be specified.

Parameters

Name Type Description Default
func (func) the function to parallelize required
iterarg (list) the variable(s) to provide to each process (see examples below) required
iterkwargs (dict) another way of providing variables to each process (see examples below) required
args (list) positional arguments for each process, the same for all processes required
kwargs (dict) keyword arguments for each process, the same for all processes required
ncpus (int/float) number of CPUs to use (if <1, treat as a fraction of the total available; if None, use loadbalancer) required
maxcpu (float) maximum CPU load; otherwise, delay the start of the next process (not used if ncpus is specified) required
maxmem (float) maximum fraction of virtual memory (RAM); otherwise, delay the start of the next process required
interval (float) number of seconds to pause between starting processes for checking load required
parallelizer (str/func) parallelization function; default ‘multiprocess’ (see below for details) required
serial (bool) whether to skip parallelization and run in serial (useful for debugging; equivalent to parallelizer='serial') required
progress (bool) whether to show a progress bar required
callback (func) an optional function to call from each worker required
globaldict (dict) an optional global dictionary to pass to each worker via the kwarg “globaldict” (note: may not update properly with low task latency) required
capture (bool) if True, capture the output of the task rather than printing it required
die (bool) whether to stop immediately if an exception is encountered (otherwise, store the exception as the result) required
lbkwargs (dict) if provided, passed to sc.loadbalancer() required
func_kwargs (dict) merged with kwargs (see above) required

Returns

Name Type Description
List of outputs from each process

Example 1 – simple usage as a shortcut to multiprocess.map():

def f(x):
    return x*x

results = sc.parallelize(f, [1,2,3])

Example 2 – simple usage for “embarrassingly parallel” processing:

import numpy as np

def rnd():
    np.random.seed()
    return np.random.random()

results = sc.parallelize(rnd, 10, ncpus=4)

Example 3 – three different equivalent ways to use multiple arguments:

def f(x,y):
    return x*y

results1 = sc.parallelize(func=f, iterarg=[(1,2),(2,3),(3,4)])
results2 = sc.parallelize(func=f, iterkwargs={'x':[1,2,3], 'y':[2,3,4]})
results3 = sc.parallelize(func=f, iterkwargs=[{'x':1, 'y':2}, {'x':2, 'y':3}, {'x':3, 'y':4}])
assert results1 == results2 == results3

Example 4 – using non-iterated arguments and dynamic load balancing:

def myfunc(i, x, y):
    np.random.seed()
    xy = [x+i*np.random.randn(100), y+i*np.random.randn(100)]
    return xy

xylist1 = sc.parallelize(myfunc, iterarg=range(5), kwargs={'x':3, 'y':8}, maxcpu=0.8, interval=0.2) # Use kwargs dict
xylist2 = sc.parallelize(myfunc, x=5, y=10, iterarg=[0,1,2], parallelizer='multiprocessing') # Supply kwargs directly and use a different parallelizer

for p,xylist in enumerate([xylist1, xylist2]):
    plt.subplot(2,1,p+1)
    for i,xy in enumerate(reversed(xylist)):
        plt.scatter(xy[0], xy[1], label='Run %i'%i)
    plt.legend()

Example 5 – using a custom parallelization function:

def f(x,y):
    return [x]*y

import multiprocessing as mp
pool = mp.Pool(processes=2)
results = sc.parallelize(f, iterkwargs=dict(x=[1,2,3], y=[4,5,6]), parallelizer=pool.map) # Note: parallelizer is pool.map, not pool

Example 6 – using Sciris as an interface to Dask:

def f(x,y):
    return [x]*y

def dask_map(task, argslist):
    import dask
    queued = [dask.delayed(task)(args) for args in argslist]
    return list(dask.compute(*queued))

results = sc.parallelize(f, iterkwargs=dict(x=[1,2,3], y=[4,5,6]), parallelizer=dask_map)

Note 1: the default parallelizer "multiprocess" uses dill for pickling, so is the most versatile (e.g., it can pickle non-top-level functions). However, it is also the slowest for passing large amounts of data. You can switch between these with parallelizer='fast' (concurrent.futures) and parallelizer='robust' (multiprocess).

The parallelizer argument allows a wide range of different parallelizers (including different aliases for each), and also supports user-supplied ones. Note that in most cases, the default parallelizer will suffice. However, the full list of options is:

- `None`, `'default'`, `'robust'`, `'multiprocess'`: the slow but robust dill-based parallelizer `multiprocess`
- `'fast'`, `'concurrent'`, `'concurrent.futures'`: the faster but more fragile pickle-based Python-default parallelizer `concurrent.futures`
- `'multiprocessing'`: the previous pickle-based Python default parallelizer, `multiprocessing`
- `'serial'`, `'serial-copy'`: no parallelization (single-threaded); with "-copy", force pickling
- `'thread'`', `'threadpool'`', `'thread-copy'`': thread- rather than process-based parallelization ("-copy" as above)
- User supplied: any `map`-like function that takes in a function and an argument list

Note 2: If parallelizing figure generation, use a non-interactive backend, or make sure (a) figure is closed inside the function call, and (b) the figure object is not returned. Otherwise, parallelization won’t increase speed (and might even be slower than serial!).

Note 3: to use on Windows, parallel calls must contained with an if __name__ == '__main__' block.

For example:

import sciris as sc

def f(x,y):
    return x*y

if __name__ == '__main__':
    results = sc.parallelize(func=f, iterarg=[(1,2),(2,3),(3,4)])
    print(results)

Note 4: In Python 3.14, the default process start method on Linux was changed from “fork” to “forkserver”. This does not use copy-on-write to share memory with worker processes but rather behaves more like “spawn” on Mac/Windows. It can also result in an EOFError when using WSL on Windows. To restore the previous behaviour, after importing sciris, set the start method to “fork” as follows:

import multiprocessing
multiprocessing.set_start_method("fork", force=True)
  • New in version 1.1.1: “serial” argument
  • New in version 2.0.0: changed default parallelizer from multiprocess.Pool to concurrent.futures.ProcessPoolExecutor; replaced maxload with maxcpu/maxmem; added returnpool argument
  • New in version 2.0.4: added “die” argument; changed exception handling
  • New in version 3.0.0: new Parallel class; propagated “die” to jobs
  • New in version 3.1.0: new “globaldict” argument
  • New in version 3.2.5: “capture” and “lbkwargs” arguments