我有一个包装方法来调用Numba兼容函数。在下面的代码中,方法
get_neighbours_wrapper()
只是调用 Numba 函数 get_neighbours_Numba()
的包装器。
我想在单独的线程上调用
neighbours.get_neighbours_wrapper(point)
,这就是我期待并行化的原因。
尽管我看到了一些性能增强 通过使函数 Numba 兼容,但我不确定它是否并行运行(很可能不是)。我怀疑调用者函数,即
get_neighbours_wrapper()
尝试访问类的成员变量,因此可能会停止真正的并行化(由于 GIL 锁?)。
import numpy as np
from numba import njit
@njit
def get_neighbours_Numba(points: np.ndarray, num_neighbors: int):
for point in points:
distances = np.zeros(num_neighbors)
neighbours_indices_xy = np.zeros((num_neighbors, 2))
## There is some further code, but not relevant for the question
return distances
class Neighbours:
def __init__(self, xy_points: np.ndarray, num_neighbors: int):
self.xy_points = xy_points
self.num_neighbors = num_neighbors
def get_neighbours_wrapper(self, point: np.ndarray):
distances = get_neighbours_Numba(self.xy_points, self.num_neighbors) # QUESTIONS: Can using class variables STOP PARALLELIZATION?
return distances
# Example usage
xy_points = np.random.rand(100, 2)
num_neighbors = 6
neighbours = Neighbours(xy_points, num_neighbors)
point = np.random.rand(2)
distances = neighbours.get_neighbours_wrapper(point)
print(distances)
问题:传递类变量是否会停止 Numba 中的并行化?如果是的话,解决办法是什么?
我想在单独的线程上调用 neighbours.get_neighbours_wrapper(point),这就是我期待并行化的原因。
请注意,在 CPython 中,线程必须使用 GIL 锁定对象,以保护解释器。这种锁定机制往往会阻止任何加速,因为除非为计算部分释放 GIL,否则线程最终会按顺序运行。
Numba
@njit
函数释放 GIL,因此多个线程中的多个 njitted 函数调用可以真正并行运行。这同样适用于 Numba njitted 函数和其他释放 GIL 的函数。话虽如此,请注意,释放 GIL 意味着您有责任确保代码不包含任何“竞争条件”。否则,结果是不确定的(例如解释器崩溃)。另请注意,在释放 GIL 的代码部分中无法访问任何 CPython 对象,尽管 njitted Numba 函数已经是这种情况。
Numba 线程只能执行不使用 CPython 对象的代码。换句话说,prange
循环不能对CPython对象进行操作,并且GIL在执行时总是被释放。
我不确定它是否并行运行Numba 函数默认是顺序的
(至少是 CPU 函数),除非您明确指定它们是并行的。这可以通过 parallel=True
JIT 标志和在循环上使用
prange
(而不是通常的 range
)来完成。单独的 JIT 标志不会自动并行化您的代码,但可以并行执行一些基本的 Numpy 函数。需要明确的是,您当前的代码是完全连续的。没有产生任何线程。
我怀疑调用者函数,即 get_neighbours_wrapper() 尝试访问类的成员变量,因此可能会停止真正的并行化(由于 GIL 锁?)。Numba njitted 函数分为两部分:包装函数和本机函数。包装函数负责将 CPython 对象转换为本机对象(以及其他方式),而本机函数则执行实际的计算工作。包装函数还在调用本机函数之前释放 GIL,并在本机函数执行后锁定 GIL。 GIL 不需要锁定即可访问本机类型的变量。
实际上,这里,Numba 会将
self.xy_points
转换为内部/Numba 特定的本机 Numpy 数组数据结构,并将
self.num_neighbors
转换为本机整数。转换后,内部 Numpy 数据结构仍然引用内存中相同的sharedNumpy 数组缓冲区。这意味着 Numpy 数组对象的数据可以在 Numba 函数中发生变化(当 GIL 释放时可能通过多个线程)。当
get_neighbours_Numba
内部代码执行时,当前线程的GIL被释放。如果从两个不同的 CPython 线程同时运行 get_neighbours_wrapper
,则只有 get_neighbours_Numba
的内部代码可以并行运行。 get_neighbours_wrapper
(隐式对象转换)中的其余代码将被锁定并因此被序列化。请注意,一小部分用于锁定 GIL 的代码可能会显着降低并行代码的可扩展性。有关这方面的更多信息,请参阅Amdahl's_law。