Python Cython并行循环问题
我使用cython计算成对距离矩阵,使用自定义度量作为快速替代 我的动机 我的度量具有以下形式Python Cython并行循环问题,python,performance,openmp,cython,Python,Performance,Openmp,Cython,我使用cython计算成对距离矩阵,使用自定义度量作为快速替代 我的动机 我的度量具有以下形式 def mymetric(u,v,w): np.sum(w * (1 - np.abs(np.abs(u - v) / np.pi - 1))**2) 使用scipy的成对距离可以计算为 x = sp.spatial.distance.pdist(r, metric=lambda u, v: mymetric(u, v, w)) 这里,r是维度为n的m-by-n向量的m矩阵,w是一个具有
def mymetric(u,v,w):
np.sum(w * (1 - np.abs(np.abs(u - v) / np.pi - 1))**2)
使用scipy的成对距离可以计算为
x = sp.spatial.distance.pdist(r, metric=lambda u, v: mymetric(u, v, w))
这里,r
是维度为n
的m
-by-n
向量的m
矩阵,w
是一个具有二次张力的“权重”因子n
因为在我的问题中,m
相当高,所以计算速度非常慢。对于m=2000
和n=10
这大约需要20秒
Cython初解
我在cython中实现了一个简单的函数来计算成对距离,并立即得到了非常有希望的结果——加速比超过500倍
import numpy as np
cimport numpy as np
import cython
from libc.math cimport fabs, M_PI
@cython.wraparound(False)
@cython.boundscheck(False)
def pairwise_distance(np.ndarray[np.double_t, ndim=2] r, np.ndarray[np.double_t, ndim=1] w):
cdef int i, j, k, c, size
cdef np.ndarray[np.double_t, ndim=1] ans
size = r.shape[0] * (r.shape[0] - 1) / 2
ans = np.zeros(size, dtype=r.dtype)
c = -1
for i in range(r.shape[0]):
for j in range(i + 1, r.shape[0]):
c += 1
for k in range(r.shape[1]):
ans[c] += w[k] * (1.0 - fabs(fabs(r[i, k] - r[j, k]) / M_PI - 1.0))**2.0
return ans
使用OpenMP时出现的问题
我想使用OpenMP进一步加快计算速度,但是,下面的解决方案大约比串行版本慢3倍
import numpy as np
cimport numpy as np
import cython
from cython.parallel import prange, parallel
cimport openmp
from libc.math cimport fabs, M_PI
@cython.wraparound(False)
@cython.boundscheck(False)
def pairwise_distance_omp(np.ndarray[np.double_t, ndim=2] r, np.ndarray[np.double_t, ndim=1] w):
cdef int i, j, k, c, size, m, n
cdef np.double_t a
cdef np.ndarray[np.double_t, ndim=1] ans
m = r.shape[0]
n = r.shape[1]
size = m * (m - 1) / 2
ans = np.zeros(size, dtype=r.dtype)
with nogil, parallel(num_threads=8):
for i in prange(m, schedule='dynamic'):
for j in range(i + 1, m):
c = i * (m - 1) - i * (i + 1) / 2 + j - 1
for k in range(n):
ans[c] += w[k] * (1.0 - fabs(fabs(r[i, k] - r[j, k]) / M_PI - 1.0))**2.0
return ans
我不知道为什么它实际上变慢了,但我尝试引入以下更改。这不仅导致性能稍差,而且结果距离ans
仅在数组开头正确计算,其余仅为零。通过这种方式实现的加速可以忽略不计
import numpy as np
cimport numpy as np
import cython
from cython.parallel import prange, parallel
cimport openmp
from libc.math cimport fabs, M_PI
from libc.stdlib cimport malloc, free
@cython.wraparound(False)
@cython.boundscheck(False)
def pairwise_distance_omp_2(np.ndarray[np.double_t, ndim=2] r, np.ndarray[np.double_t, ndim=1] w):
cdef int k, l, c, m, n
cdef Py_ssize_t i, j, d
cdef size_t size
cdef int *ci, *cj
cdef np.ndarray[np.double_t, ndim=1, mode="c"] ans
cdef np.ndarray[np.double_t, ndim=2, mode="c"] data
cdef np.ndarray[np.double_t, ndim=1, mode="c"] weight
data = np.ascontiguousarray(r, dtype=np.float64)
weight = np.ascontiguousarray(w, dtype=np.float64)
m = r.shape[0]
n = r.shape[1]
size = m * (m - 1) / 2
ans = np.zeros(size, dtype=r.dtype)
cj = <int*> malloc(size * sizeof(int))
ci = <int*> malloc(size * sizeof(int))
c = -1
for i in range(m):
for j in range(i + 1, m):
c += 1
ci[c] = i
cj[c] = j
with nogil, parallel(num_threads=8):
for d in prange(size, schedule='guided'):
for k in range(n):
ans[d] += weight[k] * (1.0 - fabs(fabs(data[ci[d], k] - data[cj[d], k]) / M_PI - 1.0))**2.0
return ans
总结
我对cython没有任何经验,只知道C语言的基本知识。如果能给我一些建议,说明这种意外行为的原因,甚至是如何更好地重新表述我的问题,我将不胜感激
最佳串行解决方案(比原始串行解决方案快10%) 最佳并行解决方案(使用8个线程,比原始并行快1%,比最佳串行快6倍)
未解决的问题: 当我尝试应用回答中提出的
累加器解决方案时,我得到以下错误:
Error compiling Cython file:
------------------------------------------------------------
...
c = i * (m - 1) - i * (i + 1) / 2 + j - 1
accumulator = 0
for k in range(n):
tmp = (1.0 - fabs(fabs(r[i, k] - r[j, k]) / M_PI - 1.0))
accumulator += w[k] * (tmp*tmp)
ans[c] = accumulator
^
------------------------------------------------------------
pdist.pyx:207:36: Cannot read reduction variable in loop body
完整代码:
@cython.cdivision(True)
@cython.wraparound(False)
@cython.boundscheck(False)
def pairwise_distance_omp(np.ndarray[np.double_t, ndim=2] r, np.ndarray[np.double_t, ndim=1] w):
cdef int i, j, k, c, size, m, n
cdef np.ndarray[np.double_t, ndim=1] ans
cdef np.double_t accumulator, tmp
m = r.shape[0]
n = r.shape[1]
size = m * (m - 1) / 2
ans = np.zeros(size, dtype=r.dtype)
with nogil, parallel(num_threads=8):
for i in prange(m, schedule='dynamic'):
for j in range(i + 1, m):
c = i * (m - 1) - i * (i + 1) / 2 + j - 1
accumulator = 0
for k in range(n):
tmp = (1.0 - fabs(fabs(r[i, k] - r[j, k]) / M_PI - 1.0))
accumulator += w[k] * (tmp*tmp)
ans[c] = accumulator
return ans
我自己没有计时,因此这可能不会有太大帮助,但是:
如果运行cython-a
获得初始尝试的注释版本(pairwise\u distance\u omp
),您会发现ans[c]+=…
行是黄色的,表明它有Python开销。看一下,对应于这条线的C表示它正在检查是否被零除。其中一个关键部分是:
if (unlikely(M_PI == 0)) {
您知道这永远不会是真的(而且在任何情况下,您可能会接受NaN值,而不是异常值)。您可以通过向函数添加以下额外的装饰器来避免此检查:
@cython.cdivision(True)
# other decorators
def pairwise_distance_omp # etc...
这将减少相当多的C代码,包括必须在单个线程中运行的代码。另一方面,这些代码中的大部分都不应该运行,编译器应该能够解决这一问题,因此目前还不清楚这会产生多大的影响
第二个建议:
# at the top
cdef np.double_t accumulator, tmp
# further down later in the loop:
c = i * (m - 1) - i * (i + 1) / 2 + j - 1
accumulator = 0
for k in range(r.shape[1]):
tmp = (1.0 - fabs(fabs(r[i, k] - r[j, k]) / M_PI - 1.0))
accumulator = accumulator + w[k] * (tmp*tmp)
ans[c] = accumulator
这有两个优点:1)tmp*tmp
可能比浮点指数快到2的幂。2) 避免读取ans
数组,这可能会有点慢,因为编译器必须始终小心其他线程没有更改它(即使您知道它不应该更改)。这当然有帮助,谢谢,但是问题仍然存在。这两个OpenMP版本现在大约比串行版本慢2倍(在8个物理内核中使用8个线程)。另外,我仍然不知道为什么第二次尝试只计算部分结果。@Ondrian我添加了第二个建议,我认为这可能会有所帮助(尽管其中一个改进也可以应用于非并行版本)。我还没有看过第二次尝试。第二个建议将串行代码的速度提高了10%,这非常好。我也设法在并行代码中应用了tmp*tmp
部分,但是,累加器部分仍然出现错误。顺便说一下,奇怪的初始计时(串行代码比并行代码快)实际上是我测量的结果!显然,time.process\u time()
不是一个好方法。对此感到尴尬。:)我现在接受答案,但如果你知道如何处理最后一个错误,我将不胜感激。如果您不这样做,我可能会创建一个新问题。acculator=acculator+w[k]*(tmp*tmp)
适合我。它感到困惑,认为它被用作OpenMP缩减(例如),但以不同的方式编写它会有所帮助。我应该在发布它之前测试它真的…这很有效!对于8个线程,大约比同等串行速度快6.5倍。非常感谢!:)更新:在我的代码中发现一个错误,当前的问题只是并行代码的性能。Ondrian-当我尝试使用累加器对矩阵列进行简单求和时,我得到了相同的“无法读取循环体中的缩减变量”错误-是什么错误导致了这个错误?@aph问题是,如果我没记错的话,使用acculator+=something
语法而不是acculator=acculator+something
。谢谢@Ondrian-这正是我的问题!
if (unlikely(M_PI == 0)) {
@cython.cdivision(True)
# other decorators
def pairwise_distance_omp # etc...
# at the top
cdef np.double_t accumulator, tmp
# further down later in the loop:
c = i * (m - 1) - i * (i + 1) / 2 + j - 1
accumulator = 0
for k in range(r.shape[1]):
tmp = (1.0 - fabs(fabs(r[i, k] - r[j, k]) / M_PI - 1.0))
accumulator = accumulator + w[k] * (tmp*tmp)
ans[c] = accumulator