传递类变量是否会停止 Numba 中的并行化?

问题描述 投票:0回答:1

我有一个包装方法来调用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 中的并行化?如果是的话,解决办法是什么?

python numba
1个回答
0
投票

我想在单独的线程上调用 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 数据结构仍然引用内存中相同的
shared
Numpy 数组缓冲区。这意味着 Numpy 数组对象的数据可以在 Numba 函数中发生变化(当 GIL 释放时可能通过多个线程)。当get_neighbours_Numba内部代码执行时,当前线程的GIL被释放。如果从两个不同的 CPython 线程同时运行
get_neighbours_wrapper
,则只有
get_neighbours_Numba
的内部代码可以并行运行。
get_neighbours_wrapper
(隐式对象转换)中的其余代码将被锁定并因此被序列化。请注意,一小部分用于锁定 GIL 的代码可能会显着降低并行代码的可扩展性。有关这方面的更多信息,请参阅
Amdahl's_law

© www.soinside.com 2019 - 2024. All rights reserved.