使用 numba 无法获得与 numpy 元素矩阵乘法相同的值

Posted

技术标签:

【中文标题】使用 numba 无法获得与 numpy 元素矩阵乘法相同的值【英文标题】:Can't get same values as numpy elementwise matrix multiplication using numba 【发布时间】:2018-03-22 18:01:20 【问题描述】:

我一直在玩 numba 并尝试实现一个简单的元素矩阵乘法。使用“vectorize”时,我得到与 numpy 乘法相同的结果,但当我使用“cuda.jit”时,它们不一样。其中许多是零。为此,我提供了一个最低限度的工作示例。任何有关该问题的帮助将不胜感激。我正在使用 numba o.35.0 和 python 2.7

from __future__ import division
from __future__ import print_function

import numpy as np

from numba import vectorize, cuda, jit

M = 80
N = 40
P = 40

# Set the number of threads in a block
threadsperblock = 32

# Calculate the number of thread blocks in the grid
blockspergrid = (M*N*P + (threadsperblock - 1)) // threadsperblock

@vectorize(['float32(float32,float32)'], target='cuda')
def VectorMult3d(a, b):
   return a*b

@cuda.jit('void(float32[:, :, :], float32[:, :, :], float32[:, :, :])')
def mult_gpu_3d(a, b, c):
  [x, y, z] = cuda.grid(3)
  if x < c.shape[0] and y < c.shape[1] and z < c.shape[2]:
    c[x, y, z] = a[x, y, z] * b[x, y, z]

if __name__ == '__main__':
  A = np.random.normal(size=(M, N, P)).astype(np.float32)
  B = np.random.normal(size=(M, N, P)).astype(np.float32)

  numpy_C = A*B

  A_gpu = cuda.to_device(A)
  B_gpu = cuda.to_device(B)
  C_gpu = cuda.device_array((M,N,P), dtype=np.float32) # cuda.device_array_like(A_gpu)

  mult_gpu_3d[blockspergrid,threadsperblock](A_gpu,B_gpu,C_gpu)

  cudajit_C = C_gpu.copy_to_host()

  print('------- using cuda.jit -------')
  print('Is close?: '.format(np.allclose(numpy_C,cudajit_C)))
  print(' of  elements are close'.format(np.sum(np.isclose(numpy_C,cudajit_C)), M*N*P))
  print('------- using cuda.jit -------\n')


  vectorize_C_gpu = VectorMult3d(A_gpu, B_gpu)
  vectorize_C = vectorize_C_gpu.copy_to_host()

  print('------- using vectorize -------')
  print('Is close?: '.format(np.allclose(numpy_C,vectorize_C)))
  print(' of  elements are close'.format(np.sum(np.isclose(numpy_C,vectorize_C)), M*N*P))
  print('------- using vectorize -------\n')

  import numba; print("numba version: "+numba.__version__)

【问题讨论】:

【参考方案1】:

您可以通过以下方式进行调试。

考虑一个更小、更简化的示例:

减少了数组大小,例如(2, 3, 1)(这样您就可以实际打印这些值并能够读取它们) 简单且确定性的内容,例如“所有的”(在运行中进行比较) 用于调试的其他内核参数
from __future__ import (division, print_function)

import numpy as np
from numba import cuda

M = 2
N = 3
P = 1

threadsperblock = 1
blockspergrid = (M * N * P + (threadsperblock - 1)) // threadsperblock


@cuda.jit
def mult_gpu_3d(a, b, c, grid_ran, grid_multed):
    grid = cuda.grid(3)
    x, y, z = grid

    grid_ran[x] = 1

    if (x < c.shape[0]) and (y < c.shape[1]) and (z < c.shape[2]):
        grid_multed[x] = 1
        c[grid] = a[grid] * b[grid]


if __name__ == '__main__':
    A = np.ones((M, N, P), np.int32)
    B = np.ones((M, N, P), np.int32)

    A_gpu = cuda.to_device(A)
    B_gpu = cuda.to_device(B)
    C_gpu = cuda.to_device(np.zeros_like(A))

    # Tells whether thread at index i have ran
    grid_ran = cuda.to_device(np.zeros([blockspergrid], np.int32))

    # Tells whether thread at index i have performed multiplication
    grid_multed = cuda.to_device(np.zeros(blockspergrid, np.int32))

    mult_gpu_3d[blockspergrid, threadsperblock](
        A_gpu, B_gpu, C_gpu, grid_ran, grid_multed)

    print("grid_ran.shape    : ", grid_ran.shape)
    print("grid_multed.shape : ", grid_multed.shape)
    print("C_gpu.shape       : ", C_gpu.shape)

    print("grid_ran          : ", grid_ran.copy_to_host())
    print("grid_multed       : ", grid_multed.copy_to_host())

    C = C_gpu.copy_to_host()
    print("C transpose flat  : ", C.T.flatten())
    print("C                 : \n", C)

输出:

grid_ran.shape    :  (6,)
grid_multed.shape :  (6,)
C_gpu.shape       :  (2, 3, 1)
grid_ran          :  [1 1 1 1 1 1]
grid_multed       :  [1 1 0 0 0 0]
C transpose flat  :  [1 1 0 0 0 0]
C                 : 
 [[[1]
  [0]
  [0]]

 [[1]
  [0]
  [0]]]

可以看到设备网格形状与数组的形状不对应:网格是平面的(M*N*P),而数组都是3维的(M, N, P)。也就是说,网格的第一个维度的索引范围为0..M*N*P-10..5,在我的示例中总共有 6 个值),而数组的第一个维度仅在0..M-10..1,在我的示例中总共有 2 个值)例子)。这个错误通常会导致越界访问,但是你已经用一个条件来保护你的内核,从而减少了有问题的线程:

if (x <= c.shape[0])

该行不允许索引高于M-1(在我的示例中为1)的线程运行(嗯,有点[1]),这就是为什么没有写入任何值并且结果中会出现许多零的原因数组。

可能的解决方案:

通常,您可以使用多维内核网格配置,即 blockspergrid 的 3D 向量而不是标量 [2]。 特别是,由于元素乘法是一种映射操作并且不依赖于数组形状,您可以将所有 3 个数组展平为 1D 数组,在 1D 网格上按原样运行内核,然后将结果重新整形 [3],[ 4]。

参考资料:

[1]How to understand “All threads in a warp execute the same instruction at the same time.” in GPU? [2]Understanding CUDA grid dimensions, block dimensions and threads organization (simple explanation) [3]numpy.ndarray.flatten [4]numpy.ravel

【讨论】:

非常感谢。你的解释很清楚。我接受了使用多维内核网格配置的建议。像下面的东西。 threadsperblock = (4, 4, 4); blockspergrid_x = np.int(np.ceil(M / threadsperblock[0])) 同样设置 blockspergrid_y 和 blockspergrid_z 然后blockspergrid = (blockspergrid_x, blockspergrid_y, blockspergrid_z)。最后用blockspergridthreadsperblock 调用mult_gpu_3d。你提供的参考资料也很棒!!再次感谢。

以上是关于使用 numba 无法获得与 numpy 元素矩阵乘法相同的值的主要内容,如果未能解决你的问题,请参考以下文章

比较 Python、Numpy、Numba 和 C++ 的矩阵乘法

Python numpy:无法将 datetime64[ns] 转换为 datetime64[D](与 Numba 一起使用)

对于纯 numpy 代码,使用 numba 的收益在哪里?

如何在 numpy 中获得逐元素矩阵乘法(Hadamard 乘积)?

没有 Numpy 的矩阵求逆

加速python中的元素数组乘法