使用NumPy和PyTorch在GPU上求解线性方程组

mkshixfv  于 2023-06-23  发布在  其他
关注(0)|答案(1)|浏览(167)

我正试图尽快解决大量的线性方程组。为了找出最快的方法,我对NumPyPyTorch进行了基准测试,它们分别在CPU和GeForce 1080 GPU上(NumPy使用Numba)。结果真的让我很困惑。
这是我在Python 3.8中使用的代码:

import timeit

import torch
import numpy
from numba import njit

def solve_numpy_cpu(dim: int = 5):
    a = numpy.random.rand(dim, dim)
    b = numpy.random.rand(dim)

    for _ in range(1000):
        numpy.linalg.solve(a, b)

def solve_numpy_njit_a(dim: int = 5):
    njit(solve_numpy_cpu, dim=dim)

@njit
def solve_numpy_njit_b(dim: int = 5):
    a = numpy.random.rand(dim, dim)
    b = numpy.random.rand(dim)

    for _ in range(1000):
        numpy.linalg.solve(a, b)

def solve_torch_cpu(dim: int = 5):
    a = torch.rand(dim, dim)
    b = torch.rand(dim, 1)

    for _ in range(1000):
        torch.solve(b, a)

def solve_torch_gpu(dim: int = 5):
    torch.set_default_tensor_type("torch.cuda.FloatTensor")
    solve_torch_cpu(dim=dim)

def main():
    for f in (solve_numpy_cpu, solve_torch_cpu, solve_torch_gpu, solve_numpy_njit_a, solve_numpy_njit_b):
        time = timeit.timeit(f, number=1)
        print(f"{f.__name__:<20s}: {time:f}")

if __name__ == "__main__":
    main()

结果如下:

solve_numpy_cpu     : 0.007275
solve_torch_cpu     : 0.012244
solve_torch_gpu     : 5.239126
solve_numpy_njit_a  : 0.000158
solve_numpy_njit_b  : 1.273660

最慢的是CUDA加速的PyTorch。我验证了PyTorch是否正在使用我的GPU

import torch
torch.cuda.is_available()
torch.cuda.get_device_name(0)

返回

True
'GeForce GTX 1080'

*我可以支持这一点,在CPU上,PyTorch比NumPy慢。我无法理解的是,为什么GPU上的PyTorch速度要慢得多。不是那么重要,但实际上更令人困惑的是Numba的njit装饰器使性能下降了几个数量级, 直到你不再使用@装饰器语法。

是我的圈套吗偶尔我会收到一条奇怪的消息,说windows页面/交换文件不够大。如果我已经采取了一个完全模糊的路径来解决线性方程在GPU上,我很高兴被引导到另一个方向。

编辑

所以,我专注于Numba并稍微改变了我的基准测试。正如@max9111所建议的那样,我重写了函数来接收输入并产生输出,因为最终,这是任何人都想使用它们的目的。现在,我还为Numba加速函数执行了第一次编译运行,以便后续的计时更加公平。最后,我检查了矩阵大小的性能,并绘制了结果。
TL/DR:高达500 x500的矩阵大小,Numba加速对numpy.linalg.solve没有真正的影响。
下面是代码:

import time
from typing import Tuple

import numpy
from matplotlib import pyplot
from numba import jit

@jit(nopython=True)
def solve_numpy_njit(a: numpy.ndarray, b: numpy.ndarray) -> numpy.ndarray:
    parameters = numpy.linalg.solve(a, b)
    return parameters

def solve_numpy(a: numpy.ndarray, b: numpy.ndarray) -> numpy.ndarray:
    parameters = numpy.linalg.solve(a, b)
    return parameters

def get_data(dim: int) -> Tuple[numpy.ndarray, numpy.ndarray]:
    a = numpy.random.random((dim, dim))
    b = numpy.random.random(dim)
    return a, b

def main():
    a, b = get_data(10)
    # compile numba function
    p = solve_numpy_njit(a, b)

    matrix_size = [(x + 1) * 10 for x in range(50)]
    non_accelerated = []
    accelerated = []
    results = non_accelerated, accelerated

    for j, each_matrix_size in enumerate(matrix_size):
        for m, f in enumerate((solve_numpy, solve_numpy_njit)):
            average_time = -1.
            for k in range(5):
                time_start = time.time()
                for i in range(100):
                    a, b = get_data(each_matrix_size)
                    p = f(a, b)
                d_t = time.time() - time_start
                print(f"{each_matrix_size:d} {f.__name__:<30s}: {d_t:f}")
                average_time = (average_time * k + d_t) / (k + 1)
            results[m].append(average_time)

    pyplot.plot(matrix_size, non_accelerated, label="not numba")
    pyplot.plot(matrix_size, accelerated, label="numba")
    pyplot.legend()
    pyplot.show()

if __name__ == "__main__":
    main()

以下是结果(运行时相对于矩阵边长度):

编辑2

看到Numba在我的案例中没有太大的区别,我回到了PyTorch的基准测试。事实上,它似乎比Numpy快4倍,甚至没有使用CUDA设备。
下面是我使用的代码:

import time
from typing import Tuple

import numpy
import torch
from matplotlib import pyplot

def solve_numpy(a: numpy.ndarray, b: numpy.ndarray) -> numpy.ndarray:
    parameters = numpy.linalg.solve(a, b)
    return parameters

def get_data(dim: int) -> Tuple[numpy.ndarray, numpy.ndarray]:
    a = numpy.random.random((dim, dim))
    b = numpy.random.random(dim)
    return a, b

def get_data_torch(dim: int) -> Tuple[torch.tensor, torch.tensor]:
    a = torch.rand(dim, dim)
    b = torch.rand(dim, 1)
    return a, b

def solve_torch(a: torch.tensor, b: torch.tensor) -> torch.tensor:
    parameters, _ = torch.solve(b, a)
    return parameters

def experiment_numpy(matrix_size: int, repetitions: int = 100):
    for i in range(repetitions):
        a, b = get_data(matrix_size)
        p = solve_numpy(a, b)

def experiment_pytorch(matrix_size: int, repetitions: int = 100):
    for i in range(repetitions):
        a, b = get_data_torch(matrix_size)
        p = solve_torch(a, b)

def main():
    matrix_size = [x for x in range(5, 505, 5)]
    experiments = experiment_numpy, experiment_pytorch
    results = tuple([] for _ in experiments)

    for i, each_experiment in enumerate(experiments):
        for j, each_matrix_size in enumerate(matrix_size):
            time_start = time.time()
            each_experiment(each_matrix_size, repetitions=100)
            d_t = time.time() - time_start
            print(f"{each_matrix_size:d} {each_experiment.__name__:<30s}: {d_t:f}")
            results[i].append(d_t)

    for each_experiment, each_result in zip(experiments, results):
        pyplot.plot(matrix_size, each_result, label=each_experiment.__name__)

    pyplot.legend()
    pyplot.show()

if __name__ == "__main__":
    main()

下面是结果(运行时对矩阵边长度):

所以现在,我会坚持使用torch.solve。然而,最初的问题仍然存在:

如何利用GPU更快地求解线性方程组?

laawzig2

laawzig21#

你的分析在几个方面都是正确的,但有几个细微差别可能有助于澄清你的结果并提高你的GPU性能:

1. CPU与GPU性能一般来说,GPU操作会产生与CPU和GPU内存之间传输数据相关的开销成本。因此,GPU加速的好处通常在较大的数据集上变得明显,其中并行化的好处超过了这种开销。这种开销成本可能是GPU计算对于小矩阵较慢的原因。要利用GPU来求解线性方程组,您应该专注于更大的矩阵。
**2. Torch solve vs torch.linalg.solve**自PyTorch 1.7.0起,torch.solve函数已弃用。使用torch.linalg.solve可以获得更好的性能和更准确的结果。
3. Numba的njit性能Numba的@njit装饰器通过在导入时使用LLVM编译器基础架构生成优化的机器代码来加速Python函数。当您使用@njit装饰器时,Numba在非Python模式下编译函数,如果函数无法完全优化,则可能导致性能降低。第一次运行还将包括一个“编译”步骤,如果它包括在计时中,这可能会使它看起来慢得多。
4.高效使用CUDA内存solve_torch_gpu函数中的行torch.set_default_tensor_type("torch.cuda.FloatTensor")将默认Tensor类型设置为CUDATensor。之后创建的每个Tensor都将是CUDATensor。这可能会导致不必要的GPU内存使用,并降低计算速度。如果您在需要时直接在GPU上创建Tensor(使用.to(device),其中device是您的CUDA设备),则效率更高,可能会缩短计算时间。

以下是您的函数的修订版本,它使用torch.linalg.solve并直接在GPU上创建Tensor:

def solve_torch_gpu_optimized(dim: int = 5):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    a = torch.rand(dim, dim, device=device)
    b = torch.rand(dim, 1, device=device)

    for _ in range(1000):
        torch.linalg.solve(a, b)

相关问题