DFS 的 Cython 并行化竞争条件

Cython parallelisation race condition for DFS

我正在尝试开发一种 AI 来以最佳方式玩单人棋盘游戏。我在几个级别上使用深度优先搜索。

我试图通过多线程处理初始循环迭代所有动作并递归到游戏树中来加速它。我的想法是,每个线程会将最初可能的移动板分成块,并在单独的递归函数中进一步评估它们。所有调用的函数都是nogil

但是,我遇到的情况我只能猜测是竞争条件,因为多线程解决方案给出了不同的结果,我不确定如何修复它。

cdef struct Move:
   int x
   int y
   int score

cdef Move search( board_t& board, int prevClears, int maxDepth, int depth ) nogil:
   cdef Move bestMove
   cdef Move recursiveMove
   cdef vector[ Move ] moves = generateMoves( board )
   cdef board_t nextBoard
   cdef int i, clears

   bestMove.score = 0

   # Split the initial possible move boards amongst threads
   for i in prange( <int> moves.size(), nogil = True ):
      # Applies move and calculates the move score
      nextBoard = applyMove( board, moves[ i ], prevClears, maxDepth, depth )

      # Recursively evaluate further moves
      if maxDepth - depth > 0:
         clears = countClears( nextBoard )
         recursiveMove = recursiveSearch( nextBoard, moves[ i ], clears, maxDepth, depth + 1 )
         moves[ i ].score += recursiveMove.score

      # Update bestMove
      if moves[ i ].score > bestMove.score:
         bestMove = moves[ i ]

   return bestMove

当涉及到 prange 时,Cython 会做一些神奇的事情,这取决于微妙的事情 - 所以真的必须查看生成的 C 代码才能理解发生了什么。

据我所见,你的代码至少有两个问题。

1.问题: bestMove 未初始化。

%%cython -+
cdef struct Move:
   ...

def foo()
   cdef Move bestMove
   return bestMove

将产生以下 C 代码:

...
struct __pyx_t_XXX_Move __pyx_v_bestMove;
...
__pyx_r = __pyx_convert__to_py_struct____pyx_t_XXX_Move(__pyx_v_bestMove); if ...
return __pyx_r;

局部变量 __pyx_v_bestMove 将保持未初始化状态(参见 SO-post),即使初始值很可能只包含零。

例如 bestMove 是一个 int,Cython 会发出警告,但它不会对结构发出警告。

2。问题: 分配 bestMove 导致竞态条件。

顺便说一句,结果可能不仅不是最佳着法,甚至可能是非法着法,因为它可能是组合 (x-,y-,score - 来自其他指定合法移动的不同合法移动的值。

这是该问题的较小复制者:

%%cython -c=-fopenmp --link-args=-fopenmp
# cython
cimport cython
from cython.parallel import prange

cdef struct A:
    double a

@cython.boundscheck(False)
def search_max(double[::1] vals):
    cdef A max_val = [-1.0] # initialized!
    cdef int i
    cdef int n = len(vals)
    for i in prange(n, nogil=True):
        if(vals[i]>max_val.a):
            max_val.a = vals[i]
    return max_val.a

Were max_val a cdef double Cython 不会构建它,因为它会尝试使 max_val 私有化(巧妙的魔法)。但是现在,max_val 在线程之间共享(参见生成的 C 代码)并且应该保护对它的访问。如果不是,我们可以看到(可能需要 运行 多次来触发竞争条件)结果:

>>> import numpy as np
>>> a = np.random.rand(1000)
>>> search_max(a)-search_max(a)
#0.0006253360398751351 but should be 0.0

可以做什么?正如@DavidW 所建议的那样,我们可以收集每个线程的最大值,然后在 post 处理步骤中找到绝对最大值 - 请参阅此 ,这将导致:

%%cython -+ -c=-fopenmp --link-args=-fopenmp

cimport cython
from cython.parallel import prange, threadid
from libcpp.vector cimport vector
cimport openmp

cdef struct A:
    double a

@cython.boundscheck(False)
def search_max(double[::1] vals):
    cdef int i, tid
    cdef int n = len(vals)
    cdef vector[A] max_vals
    # every thread gets its own max value:
    NUM_THREADS = 4
    max_vals.resize(NUM_THREADS, [-1.0])
    for i in prange(n, nogil=True, num_threads = NUM_THREADS):
        tid = threadid()
        if(vals[i]>max_vals[tid].a):
            max_vals[tid].a = vals[i]

    #post process, collect results of threads:
    cdef double res = -1.0
    for i in range(NUM_THREADS):
        if max_vals[i].a>res:
            res = max_vals[i].a

    return res

我认为将 openmp 功能与 C/C++ 一起使用并用 Cython 包装生成的代码更容易且更不容易出错:Cython 不仅没有 ,而且在在查看简单的 C 代码时,并行代码已经够难了,没有 Cython 做的任何隐式魔法。