Python 这个问题可以用OpenMP在Cython中并行实现吗?
我用OpenMP对一些Cython代码进行了视差分析。有时,代码会计算错误的结果 我为我的问题创建了一个几乎最小的工作示例。几乎是这样,因为错误结果的频率似乎取决于代码中哪怕是最微小的更改,因此,例如,我保留了函数指针 Cython代码是Python 这个问题可以用OpenMP在Cython中并行实现吗?,python,parallel-processing,openmp,cython,Python,Parallel Processing,Openmp,Cython,我用OpenMP对一些Cython代码进行了视差分析。有时,代码会计算错误的结果 我为我的问题创建了一个几乎最小的工作示例。几乎是这样,因为错误结果的频率似乎取决于代码中哪怕是最微小的更改,因此,例如,我保留了函数指针 Cython代码是 #cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True # distutils: language = c++ import numpy as np ci
#cython: language_level=3, boundscheck=False, wraparound=False, cdivision=True
# distutils: language = c++
import numpy as np
cimport cython
from cython.parallel import prange, parallel
from libcpp.vector cimport vector
cimport numpy as np
cdef inline double estimator_matheron(const double f_diff) nogil:
return f_diff * f_diff
ctypedef double (*_estimator_func)(const double) nogil
cdef inline void normalization_matheron(
vector[double]& variogram,
vector[long]& counts,
const int variogram_len
):
cdef int i
for i in range(variogram_len):
if counts[i] == 0:
counts[i] = 1
variogram[i] /= (2. * counts[i])
ctypedef void (*_normalization_func)(vector[double]&, vector[long]&, const int)
def test(const double[:] f):
cdef _estimator_func estimator_func = estimator_matheron
cdef _normalization_func normalization_func = normalization_matheron
cdef int i_max = f.shape[0] - 1
cdef int j_max = i_max + 1
cdef vector[double] variogram_local, variogram
cdef vector[long] counts_local, counts
cdef int i, j
with nogil, parallel():
variogram_local.resize(j_max, 0.0)
counts_local.resize(j_max, 0)
for i in range(i_max):
for j in range(1, j_max-i):
counts_local[j] += 1
variogram_local[j] += estimator_func(f[i] - f[i+j])
normalization_func(variogram_local, counts_local, j_max)
return np.asarray(variogram_local)
为了测试代码,我使用了以下脚本:
import numpy as np
from cython_parallel import test
z = np.array(
(41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3),
dtype=np.double,
)
print(test(z))
结果应该是
[0. 0.49166667 0.7625 1.09071429 0.90166667 1.336
0.9525 0.435 0.005 0.405 ]
这就是错误结果通常的样子
[0. 0.44319444 0.75483871 1.09053571 0.90166667 1.336
0.9525 0.435 0.005 0.405 ]
此代码主要将数字汇总到向量变异函数中。大多数情况下,这段代码是有效的,但如果没有足够的统计数据,可能每30次都会产生错误的结果。如果我用nogil将行更改为parallel:to with nogil:,它总是有效的。如果我根本不使用函数指针,它也总是有效的,如下所示:
with nogil, parallel():
variogram_local.resize(j_max, 0.0)
counts_local.resize(j_max, 0)
for i in range(i_max):
for j in range(1, j_max-i):
counts_local[j] += 1
variogram_local[j] += (f[i] - f[i+j]) * (f[i] - f[i+j])
for j in range(j_max):
if counts_local[j] == 0:
counts_local[j] = 1
variogram_local[j] /= (2. * counts_local[j])
return np.asarray(variogram_local)
variogram.resize(j_max, 0.0)
counts.resize(j_max, 0)
with nogil, parallel():
for i in range(i_max):
for j in prange(1, j_max-i):
counts[j] += 1
variogram[j] += estimator_func(f[i] - f[i+j])
完整的代码在不同的平台上测试,这些问题主要发生在带有clang的MacOS上,例如:
编辑
多亏了您的输入,我修改了代码,使用num_threads=2,它就可以工作了。但是一旦num_threads>2,我就会再次得到错误的结果。你认为,如果Cython对OpenMP的支持是完美的,我的新代码应该可以工作,还是我仍然有问题?
如果这应该在Cython的一边,我想我会在纯C++中实现代码。
def test(const double[:] f):
cdef int i_max = f.shape[0] - 1
cdef int j_max = i_max + 1
cdef vector[double] variogram_local, variogram
cdef vector[long] counts_local, counts
cdef int i, j, k
variogram.resize(j_max, 0.0)
counts.resize(j_max, 0)
with nogil, parallel(num_threads=2):
variogram_local = vector[double](j_max, 0.0)
counts_local = vector[long)(j_max, 0)
for i in prange(i_max):
for j in range(1, j_max-i):
counts_local[j] += 1
variogram_local[j] += (f[i] - f[i+j]) * (f[i] - f[i+j])
for k in range(j_max):
counts[k] += counts_local[k]
variogram[k] += variogram_local[k]
for i in range(j_max):
if counts[i] == 0:
counts[i] = 1
variogram[i] /= (2. * counts[i])
return np.asarray(variogram)
与它们的名字相反,变异函数和计数实际上并不是局部的。它们是共享的,所有线程都并行地处理它们,因此结果是未定义的
请注意,您实际上没有共享任何工作。它只是所有线程都在做相同的事情——整个串行任务
一个合理的并行版本看起来更像这样:
with nogil, parallel():
variogram_local.resize(j_max, 0.0)
counts_local.resize(j_max, 0)
for i in range(i_max):
for j in range(1, j_max-i):
counts_local[j] += 1
variogram_local[j] += (f[i] - f[i+j]) * (f[i] - f[i+j])
for j in range(j_max):
if counts_local[j] == 0:
counts_local[j] = 1
variogram_local[j] /= (2. * counts_local[j])
return np.asarray(variogram_local)
variogram.resize(j_max, 0.0)
counts.resize(j_max, 0)
with nogil, parallel():
for i in range(i_max):
for j in prange(1, j_max-i):
counts[j] += 1
variogram[j] += estimator_func(f[i] - f[i+j])
共享数组在外部初始化,然后线程共享内部j循环。因为没有两个线程在同一个j上工作,所以这样做是安全的
现在,将内部循环并行化可能并不理想。如果要实际并行化外部循环,实际上必须生成实际的局部变量,然后合并/减少它们。与它们的名称相反,变异函数和计数实际上不是局部的。它们是共享的,所有线程都并行地处理它们,因此结果是未定义的
请注意,您实际上没有共享任何工作。它只是所有线程都在做相同的事情——整个串行任务
一个合理的并行版本看起来更像这样:
with nogil, parallel():
variogram_local.resize(j_max, 0.0)
counts_local.resize(j_max, 0)
for i in range(i_max):
for j in range(1, j_max-i):
counts_local[j] += 1
variogram_local[j] += (f[i] - f[i+j]) * (f[i] - f[i+j])
for j in range(j_max):
if counts_local[j] == 0:
counts_local[j] = 1
variogram_local[j] /= (2. * counts_local[j])
return np.asarray(variogram_local)
variogram.resize(j_max, 0.0)
counts.resize(j_max, 0)
with nogil, parallel():
for i in range(i_max):
for j in prange(1, j_max-i):
counts[j] += 1
variogram[j] += estimator_func(f[i] - f[i+j])
共享数组在外部初始化,然后线程共享内部j循环。因为没有两个线程在同一个j上工作,所以这样做是安全的
现在,将内部循环并行化可能并不理想。如果要实际并行化外循环,实际上必须生成实际的局部变量,然后合并/减少它们。修改后的代码的问题是,您有一个竞争条件,该部分将计数_local和变异函数_local相加。您希望在并行块中使用它,这样您仍然可以访问线程局部变量,但一次只需要一个线程来处理它。最简单的方法是将其放入with gil:块中,以便Python一次执行一个线程:
with gil:
for k in range(j_max):
counts[k] += counts_local[k]
variogram[k] += variogram_local[k]
这一点在最后应该是一个快速的任务,所以不应该花费太长时间
如果是在C/C++中,您可能会对块使用pragma openmp-atomic或pragma openmp-critical。在Cython中很难做到这一点,因为他们的OpenMP支持非常基本,但您可能会滥用包装的C宏使添加原子化
Cython的OpenMP支持实际上是围绕简单循环和标量缩减而设计的。如果你做的比这还多,那么它就没有语法来控制OpenMP,因此我倾向于建议你在C或C++中写你最喜欢的OpenMP函数,不管你用哪个更舒服。 < p>你的修改代码的问题是你有一个竞争条件,这个部分加上了CuthSux本地和局部变异函数。您希望在并行块中使用它,这样您仍然可以访问线程局部变量,但一次只需要一个线程来处理它。最简单的方法是将其放入with gil:块中,以便Python一次执行一个线程:
with gil:
for k in range(j_max):
counts[k] += counts_local[k]
variogram[k] += variogram_local[k]
这一点在最后应该是一个快速的任务,所以不应该花费太长时间
如果是在C/C++中,您可能会对块使用pragma openmp-atomic或pragma openmp-critical。在Cython中很难做到这一点,因为他们的OpenMP支持非常基本,但您可能会滥用包装的C宏使添加原子化
Cython的OpenMP支持实际上是围绕简单循环和标量缩减而设计的。如果你做的比这还多,那么它就没有语法来给OpenMP提供精细的控制,因此,我倾向于推荐你在C或C++中编写你的关键OpenMP函数,不管你是否更舒服。
能够。非常感谢您非常清楚的回答。天真地说,我一直认为OpenMP的并行化很容易。从我读到的资料来看,目前在Cython中甚至不可能以这种方式使用局部变量。你有什么进一步的提示吗?我怀疑如果你在并行块内分配到*_local,例如变差函数_local=vector[double]j_max,它将变成线程本地。然而,由于Cython隐式地做出这些决定,因此如果不查看生成的代码,就很难知道它做了什么。通常最好是在C/C++中编写并行循环,您可以自己控制这些东西,然后从cythonasignments调用它,事实上,在并行块中使变量成为局部线程。但是,您需要考虑这些变量在并行块之后是不可用的。非常感谢您的非常明确的答案。天真地说,我一直认为OpenMP的并行化很容易。从我读到的资料来看,目前在Cython中甚至不可能以这种方式使用局部变量。你有什么进一步的提示吗?我怀疑如果你在并行块内分配到*_local,例如变差函数_local=vector[double]j_max,它将变成线程本地。然而,由于Cython隐式地做出这些决定,因此如果不查看生成的代码,就很难知道它做了什么。通常最好是在C/C++中编写并行循环,您可以自己控制这些东西,然后从cythonasignments调用它,事实上,在并行块中使变量成为局部线程。但是,你需要考虑这些变量在并行块之后是不可用的。此外,@ Zulan所说的,在平行部分之后,用**局部变量调用正规化函数,所以如果它们是适当的局部,那么不管怎样,你都会得到一个胡说八道的结果。在并行部分之后调用normalization_func,但是使用*_局部变量,因此如果它们是适当的局部变量,那么无论如何都会得到一个无意义的结果