python--用 OpenMP 并行多核加速 Python!

转自:http://blog.csdn.net/gzlaiyonghao/article/details/6670128/

赖勇浩(http://laiyonghao.com

注:
0、读懂这篇文章需要了解 OpenMP 基本用法。
1、读懂这篇文章需要了解 GIL 基本概念。
2、基本上是这篇的翻译: http://docs.cython.org/src/userguide/parallelism.html,标题是我自己取的,如有错漏、不明,敬请参详原文。
3、本篇不是使用 cython.parallel 的指南(或手册),仅作信息传播之用。
4、我之前翻译过一篇文章《OpenMP与C++:事半功倍地获得多线程的好处》有助于理解这篇文章,见:上( http://blog.csdn.net/lanphaday/article/details/1503817),下( http://blog.csdn.net/lanphaday/article/details/1507834)。

Cython 0.15 新增了 cython.parallel 模块,实现对原生并行编程的支持。现在只支持 OpenMP,以后会加入更多的后端支持。需要注意的是并行是运行在释放了 GIL 的环境下的。

cython.parallel.prange([start], stop[, step], nogil=False, schedule=None)

此函数并行循环,OpenMP 自动构建线程池,并根据指定的调度方案分派作业给这些线程。step 参数不可为 0,如果 nogil 参数为 true,那么这个循环就会被包装在一个 nogil 环境中。shedule 参数支持 static/dynamic/guided/auto/runtime 等 OpenMP 中定义的调度机制。
thread-locality 和 reduction 是从变量进来推断决定的。在 prange 块中被赋值的变量,会被看作 lastprivate,意思是这个变量的值会是最后一次迭代的值。如果对变量使用了原地操作符,那它会被看作 reduction,意思是每条线程都拷贝了一个私有变量,然后在循环结束后应用这个操作符,并赋值给原来的变量。索引变量总是 lastprivate,而在并行块中被赋值的变量都会被看作 private,而且在离开并行块后不可用,因为无法确定它的最后的值。(译注:对这两段理解不能的话,需要阅读 OpenMP 相关文档)。
下面是一个关于 reduction 的例子:

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

cdef int i
cdef int sum = 0

for i in prange(n, nogil=True):
    sum += i

print sum
再来一个共享 numpy 数组的例子:
  1. from cython.parallel import *  
  2.   
  3. def func(np.ndarray[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]  
from cython.parallel import *

def func(np.ndarray[double] x, double alpha):
    cdef Py_ssize_t i

    for i in prange(x.shape[0]):
        x[i] = alpha * x[i]

cython.parallel.parallel()

可以在 with 语句中使用这个指令来实现代码序列的并行执行。这在为 prange 准备 thread-local 的缓冲区时非常有用。内含的 prange 将成为不并行的工作共享循环,所以一切在并行 section 中被赋值的变量在 prange 中也是 private。所有并行块中的 private 变量在离开并行块后都不可用。
thread-local 缓冲的例子:

  1. from cython.parallel import *  
  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 == NULL:  
  11.         abort()  
  12.   
  13.     # populate our local buffer in a sequential loop   
  14.     for idx in range(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)  
from cython.parallel import *
from libc.stdlib cimport abort, malloc, free

cdef Py_ssize_t idx, i, n = 100
cdef int * local_buf
cdef size_t size = 10

with nogil, parallel():
    local_buf = <int *> malloc(sizeof(int) * size)
    if local_buf == NULL:
        abort()

    # populate our local buffer in a sequential loop
    for idx in range(size):
        local_buf[i] = i * 2

    # share the work using the thread-local buffer(s)
    for i in prange(n, schedule='guided'):
        func(local_buf)

    free(local_buf)
以后 sections 将支持并行块,这样可以把 sections 的代码分配给多个线程执行。

cython.parallel.threadid()

返回线程 ID,对于 n 个线程,它们的 ID 范围是 [0, n)。

编译

要启用 OpenMP 支持,需要把 C 或 C++ 编译器的 OpenMP 开关打开,gcc 适用的 setup.py 如下:

  1. from distutils.core import setup  
  2. from distutils.extension import Extension  
  3. from Cython.Distutils import build_ext  
  4.   
  5. ext_module = Extension(  
  6.     "hello",  
  7.     ["hello.pyx"],  
  8.     extra_compile_args=['-fopenmp'],  
  9.     extra_link_args=['-fopenmp'],  
  10. )  
  11.   
  12. setup(  
  13.     name = 'Hello world app',  
  14.     cmdclass = {'build_ext': build_ext},  
  15.     ext_modules = [ext_module],  
  16. )  
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_module = Extension(
    "hello",
    ["hello.pyx"],
    extra_compile_args=['-fopenmp'],
    extra_link_args=['-fopenmp'],
)

setup(
    name = 'Hello world app',
    cmdclass = {'build_ext': build_ext},
    ext_modules = [ext_module],
)

打断

nogil 模式下的并行的 with 和 prange 块支持 break、continue 和 return。此外,还能够在这些块中使用 with gil 块,也可以抛出异常。但是,因为使用了 OpenMP,不能跳出了事,最好还是退出程序。以 prange() 为例,在第一次 return、break 或抛出异常后,所有线程的每一次循环都会跳过。所以如果有多个值应当返回时该返回哪个值是没有定义的,因为迭代本身是没有特定的顺序的:

  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  
from cython.parallel import prange

cdef int func(Py_ssize_t n):
    cdef Py_ssize_t i

    for i in prange(n, nogil=True):
        if i == 8:
            with gil:
                raise Exception()
        elif i == 4:
            break
        elif i == 2:
            return i
上例中到底是抛出异常,还是简单地 break 又或者返回 2,是没有定义的(不确定的)。

嵌套并行

因为 gcc 的一个 bug,现在嵌套并行被禁用掉了,不过,你可以在一个并行段中调用含有并行段的函数。

参考资料

[1] http://www.openmp.org/mp-documents/spec30.pdf
[2] http://gcc.gnu.org/bugzilla/show_bug.cgi?id=49897

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值