HOWTO: Run Python in Parallel

We can improve performace of python calculation by running python in parallel. In this turtorial we will be making use of the multithreading library to run python code in parallel.

Multiprocessing is part of the standard python library distribution on versions python/2.6 and above so no additonal instalation is required (Owens and Pitzer both offer 2.7 and above so this should not be an issue). However, we do recommend you use python environments when using multiple libraries to avoid version conflicts with different projects you may have. See here for more information.

Please note that this parallelization is limited to a single node. If you need to run your job across multiple nodes, you should consider other options like mpi4py.

Pool

One way to parallelizing is by created a parallel pool. This can be done by using the Pool method:

p = Pool(10)

This will create a pool of 10 worker processes.

Once you have a pool of worker processes created you can then use the map method to assign tasks to each worker.

p.map(my_function, something_iterable)

Here is an example python code:

from multiprocessing import Pool
from timeit import default_timer as timer
import time


def sleep_func(x):
        time.sleep(x)


if __name__ == '__main__':

        arr = [1,1,1,1,1]

        # create a pool of 5 worker processes
        p = Pool(5)

        start = timer()

        # assign sleep_func to a worker for each entry in arr.
        # each array entry is passed as an argument to sleep_func
        p.map(sleep_func, arr)

        print("parallel time: ", timer() - start)


        start = timer()
        # run the functions again but in serial
        for a in arr:
            sleep_func(a)
        print("serial time: ", timer() - start)

The above code was then submitted using the below job script:

#!/bin/bash

#SBATCH --account <your-project-id>
#SBATCH --job-name Python_ExampleJob
#SBATCH --nodes=1
#SBATCH --time=00:10:00

module load python

python example_pool.py

After submitting the above job, the following was the output:

parallel time:  1.003282466903329
serial time:  5.005984931252897

See the documenation for more details and examples on using Pool.

Process

The mutiprocessing library also provides the Process method to run functions asynchronously.

 

To create a Process object you can simply make a call to: 

proc = Process(target=my_function, args=[my_function, arguments, go, here])

The target is set equal to the name of your function which you want to run asynchronously and args is a list of arguement for your function.

Start running a process asynchronously by:

proc.start()

Doing so will begin running the function in another process and the main parent process will continue in its execution.

You can make the parent process wait for a child process to finish with:

proc.join()

 

If you use proc.run() it will run your process and wait for it to finish before continuing on in executing the parent process. 

Note: The below code will start proc2 only after proc1 has finshed. If you want to start multiple processes and wait for them use start() and join() instead of run.

proc1.run()
proc2.run()

Examples

Here some example code:

from multiprocessing import Process
from timeit import default_timer as timer
import time

def sleep_func(x):
        print(f'Sleeping for {x} sec')
        time.sleep(x)

if __name__ == '__main__':
        
        # initialize process objects
        proc1 = Process(target=sleep_func, args=[1])
        proc2 = Process(target=sleep_func, args=[1])
        
        # begin timer
        start = timer()
        
        # start processes
        proc1.start()
        proc2.start()
        
        # wait for both process to finish
        proc1.join()
        proc2.join()
        
        print('Time: ', timer() - start)
        

Running this code give the following output:

Sleeping for 1 sec
Sleeping for 1 sec
Time:  1.0275288447737694

 

You can create a many process easily in loop aswell:

from multiprocessing import Process
from timeit import default_timer as timer
import time

def sleep_func(x):
        print(f'Sleeping for {x} sec')
        time.sleep(x)

if __name__ == '__main__':
        
        # empty list to later store processes 
        processes = []
        
        # start timer
        start = timer()
        
       
        for i in range(10):
            # initialize and start processes
            p = Process(target=sleep_func, args=[5])
            p.start()
  
            # add the processes to list for later reference
            processes.append(p)
        
        # wait for processes to finish.
        # we cannot join() them within the same loop above because it would 
        # wait for the process to finish before looping and creating the next one. 
        # So it would be the same as running them sequentially.
        for p in processes:
            p.join()
        
        print('Time: ', timer() - start)
        
  

Output:

Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Sleeping for 5 sec
Time:  5.069192241877317

See documentation for more information and example on using Process.

Shared States

When running process in parallel it is generally best to avoid sharing states between processes. However, if data must be shared see documentation for more information and examples on how to safely share data.

Other Resources

  • Spark:You can also drastically improve preformance of your python code by using Apache Spark. See Spark for more details.
  • Horovod: If you are using Tensorflow, PyTorch or other python machine learning packages you may want to also consider using Horovod. Horovod will take single-GPU training scripts and scale it to train across many GPUs in parallel.
Supercomputer: 
Service: 
Fields of Science: