Using Parallelism

Cython supports native parallelism through the cython.parallelmodule. To use this kind of parallelism, the GIL must be released(see Releasing the GIL).It currently supports OpenMP, but later on more backends might be supported.

Note

Functionality in this module may only be used from the main threador parallel regions due to OpenMP restrictions.

  • cython.parallel.prange([start,] stop[, step][, nogil=False][, schedule=None[, chunksize=None]][, num_threads=None])
  • This function can be used for parallel loops. OpenMP automaticallystarts a thread pool and distributes the work according to the scheduleused.

Thread-locality and reductions are automatically inferred for variables.

If you assign to a variable in a prange block, it becomes lastprivate, meaning that thevariable will contain the value from the last iteration. If you use aninplace operator on a variable, it becomes a reduction, meaning that thevalues from the thread-local copies of the variable will be reduced withthe operator and assigned to the original variable after the loop. Theindex variable is always lastprivate.Variables assigned to in a parallel with block will be private and unusableafter the block, as there is no concept of a sequentially last value.

Parameters:

  • start – The index indicating the start of the loop (same as the start argument in range).
  • stop – The index indicating when to stop the loop (same as the stop argument in range).
  • step – An integer giving the step of the sequence (same as the step argument in range).It must not be 0.
  • nogil – This function can only be used with the GIL released.If nogil is true, the loop will be wrapped in a nogil section.
  • schedule
    The schedule is passed to OpenMP and can be one of the following:

    • static:
    • If a chunksize is provided, iterations are distributed to allthreads ahead of time in blocks of the given chunksize. If nochunksize is given, the iteration space is divided into chunks thatare approximately equal in size, and at most one chunk is assignedto each thread in advance.
      This is most appropriate when the scheduling overhead matters andthe problem can be cut down into equally sized chunks that areknown to have approximately the same runtime.

    • dynamic:

    • The iterations are distributed to threads as they request them,with a default chunk size of 1.
      This is suitable when the runtime of each chunk differs and is notknown in advance and therefore a larger number of smaller chunksis used in order to keep all threads busy.

    • guided:

    • As with dynamic scheduling, the iterations are distributed tothreads as they request them, but with decreasing chunk size. Thesize of each chunk is proportional to the number of unassignediterations divided by the number of participating threads,decreasing to 1 (or the chunksize if provided).
      This has an advantage over pure dynamic scheduling when it turnsout that the last chunks take more time than expected or areotherwise being badly scheduled, so that most threads start runningidle while the last chunks are being worked on by only a smallernumber of threads.

    • runtime:

    • The schedule and chunk size are taken from the runtime schedulingvariable, which can be set through the openmp.omp_set_schedule()function call, or the OMP_SCHEDULE environment variable. Note thatthis essentially disables any static compile time optimisations ofthe scheduling code itself and may therefore show a slightly worseperformance than when the same scheduling policy is staticallyconfigured at compile time.The default schedule is implementation defined. For more information consultthe OpenMP specification [1].
  • num_threads – The num_threads argument indicates how many threads the team should consist of. If not given,OpenMP will decide how many threads to use. Typically this is the number of cores available onthe machine. However, this may be controlled through the omp_set_num_threads() function, orthrough the OMP_NUM_THREADS environment variable.
  • chunksize – The chunksize argument indicates the chunksize to be used for dividing the iterations among threads.This is only valid for static, dynamic and guided scheduling, and is optional. Different chunksizesmay give substantially different performance results, depending on the schedule, the load balance it provides,the scheduling overhead and the amount of false sharing (if any).

Example with a reduction:

  1. from cython.parallel import prange
  2.  
  3. cdef int i
  4. cdef int n = 30
  5. cdef int sum = 0
  6.  
  7. for i in prange(n, nogil=True):
  8. sum += i
  9.  
  10. print(sum)

Example with a typed memoryview (e.g. a NumPy array):

  1. from cython.parallel import prange
  2.  
  3. def func(double[:] x, double alpha):
  4. cdef Py_ssize_t i
  5.  
  6. for i in prange(x.shape[0]):
  7. x[i] = alpha * x[i]
  • cython.parallel.parallel(num_threads=None)
  • This directive can be used as part of a with statement to execute codesequences in parallel. This is currently useful to setup thread-localbuffers used by a prange. A contained prange will be a worksharing loopthat is not parallel, so any variable assigned to in the parallel sectionis also private to the prange. Variables that are private in the parallelblock are unavailable after the parallel block.

Example with thread-local buffers:

  1. from cython.parallel import parallel, prange
  2. from libc.stdlib cimport abort, malloc, free
  3.  
  4. cdef Py_ssize_t idx, i, n = 100
  5. cdef int * local_buf
  6. cdef size_t size = 10
  7.  
  8. with nogil, parallel():
  9. local_buf = <int *> malloc(sizeof(int) * size)
  10. if local_buf is NULL:
  11. abort()
  12.  
  13. # populate our local buffer in a sequential loop
  14. for i in xrange(size):
  15. local_buf[i] = i * 2
  16.  
  17. # share the work using the thread-local buffer(s)
  18. for i in prange(n, schedule='guided'):
  19. func(local_buf)
  20.  
  21. free(local_buf)

Later on sections might be supported in parallel blocks, to distributecode sections of work among threads.

  • cython.parallel.threadid()
  • Returns the id of the thread. For n threads, the ids will range from 0 ton-1.

Compiling

To actually use the OpenMP support, you need to tell the C or C++ compiler toenable OpenMP. For gcc this can be done as follows in a setup.py:

  1. from setuptools import Extension, setup
  2. from Cython.Build import cythonize
  3.  
  4. ext_modules = [
  5. Extension(
  6. "hello",
  7. ["hello.pyx"],
  8. extra_compile_args=['-fopenmp'],
  9. extra_link_args=['-fopenmp'],
  10. )
  11. ]
  12.  
  13. setup(
  14. name='hello-parallel-world',
  15. ext_modules=cythonize(ext_modules),
  16. )

For Microsoft Visual C++ compiler, use '/openmp' instead of '-fopenmp'.

Breaking out of loops

The parallel with and prange blocks support the statements break, continue andreturn in nogil mode. Additionally, it is valid to use a with gil blockinside these blocks, and have exceptions propagate from them.However, because the blocks use OpenMP, they can not just be left, so theexiting procedure is best-effort. For prange() this means that the loopbody is skipped after the first break, return or exception for any subsequentiteration in any thread. It is undefined which value shall be returned ifmultiple different values may be returned, as the iterations are in noparticular order:

  1. from cython.parallel import prange
  2.  
  3. cdef int func(Py_ssize_t n):
  4. cdef Py_ssize_t i
  5.  
  6. for i in prange(n, nogil=True):
  7. if i == 8:
  8. with gil:
  9. raise Exception()
  10. elif i == 4:
  11. break
  12. elif i == 2:
  13. return i

In the example above it is undefined whether an exception shall be raised,whether it will simply break or whether it will return 2.

Using OpenMP Functions

OpenMP functions can be used by cimporting openmp:

  1. # tag: openmp
  2. # You can ignore the previous line.
  3. # It's for internal testing of the Cython documentation.
  4.  
  5. from cython.parallel cimport parallel
  6. cimport openmp
  7.  
  8. cdef int num_threads
  9.  
  10. openmp.omp_set_dynamic(1)
  11. with nogil, parallel():
  12. num_threads = openmp.omp_get_num_threads()
  13. # ...

References

[1]https://www.openmp.org/mp-documents/spec30.pdf