【问题标题】:Efficient dot products of large memory-mapped arrays大型内存映射数组的高效点积
【发布时间】:2014-01-25 20:36:46
【问题描述】:

我正在使用一些相当大、密集的 numpy 浮点数组,这些数组当前驻留在 PyTables CArrays 的磁盘上。我需要能够使用这些数组执行高效的点积,例如C = A.dot(B),其中A 是一个巨大的(~1E4 x 3E5 float32)内存映射数组,而BC 是较小的numpy驻留在核心内存中的数组。

我现在正在做的是使用np.memmap 将数据复制到内存映射的numpy 数组中,然后直接在内存映射数组上调用np.dot。这可行,但我怀疑标准 np.dot(或者更确切地说是它调用的底层 BLAS 函数)在计算结果所需的 I/O 操作数量方面可能不是很有效。

我在this review article 中遇到了一个有趣的例子。使用 3x 嵌套循环计算的简单点积,如下所示:

def naive_dot(A, B, C):
    for ii in xrange(n):
        for jj in xrange(n):
            C[ii,jj] = 0
            for kk in xrange(n):
                C[ii,jj] += A[ii,kk]*B[kk,jj]
    return C

需要O(n^3) I/O 操作来计算。

但是,通过在适当大小的块中处理数组:

def block_dot(A, B, C, M):
    b = sqrt(M / 3)
    for ii in xrange(0, n, b):
        for jj in xrange(0, n, b):
            C[ii:ii+b,jj:jj+b] = 0
            for kk in xrange(0, n, b):
                C[ii:ii+b,jj:jj+b] += naive_dot(A[ii:ii+b,kk:kk+b], 
                                                B[kk:kk+b,jj:jj+b],
                                                C[ii:ii+b,jj:jj+b])
    return C

其中M 是可装入核心内存的最大元素数,I/O 操作数减少到O(n^3 / sqrt(M))

np.dot 和/或np.memmap 有多聪明?调用 np.dot 是否执行 I/O 高效的块状点积? np.memmap 是否会做任何花哨的缓存来提高此类操作的效率?

如果没有,是否有一些预先存在的库函数可以执行 I/O 高效的点积,或者我应该尝试自己实现它吗?

更新

我已经对np.dot 的手动实现进行了一些基准测试,该实现对输入数组的块进行操作,这些块被显式读入核心内存。该数据至少部分解决了我最初的问题,因此我将其发布为答案。

【问题讨论】:

  • SWAG:你在谷歌代码上查看过numexprat the Cheese factory吗?
  • @MarkMikofski 谢谢,但这并不是我真正想要的那种东西 - 首先因为我想对整个矩阵而不是元素操作进行快速线性代数运算,其次因为我在这种情况下,主要是 I/O 限制而不是 CPU 限制。
  • @MarkMikofski 不,当我说我是“I/O-bound”时,我的意思是放慢我速度的主要因素是必须将数据从硬盘读取到系统内存中。如果限制因素首先是从硬盘读取数据,那么能够并行处理事情根本不会真正加快速度。
  • @J.F.Sebastian 我正在尝试实现this algorithm 来逼近大型矩阵的 SVD。我认为没有矩阵乘法就没有办法。
  • @usethedeathstar 1) 我还没有尝试过np.einsum,因为我想不出它可能比np.dot 更快的任何特定原因。对于计算核心内存中两个数组的点积,np.dot 将比对np.einsum 的等效调用更快,因为它可以使用更优化的 BLAS 函数。就我而言,可能几乎没有区别,因为我受 I/O 限制。 2) 不,正如我在描述中所说,它们是密集矩阵。

标签: python arrays performance numpy linear-algebra


【解决方案1】:

我不认为 numpy 优化了 memmap 数组的点积,如果你查看矩阵乘法的代码,我得到了 here,你会看到函数 MatrixProduct2(当前实现的)计算c内存顺序中结果矩阵的值:

op = PyArray_DATA(ret); os = PyArray_DESCR(ret)->elsize;
axis = PyArray_NDIM(ap1)-1;
it1 = (PyArrayIterObject *)
    PyArray_IterAllButAxis((PyObject *)ap1, &axis);
it2 = (PyArrayIterObject *)
    PyArray_IterAllButAxis((PyObject *)ap2, &matchDim);
NPY_BEGIN_THREADS_DESCR(PyArray_DESCR(ap2));
while (it1->index < it1->size) {
    while (it2->index < it2->size) {
        dot(it1->dataptr, is1, it2->dataptr, is2, op, l, ret);
        op += os;
        PyArray_ITER_NEXT(it2);
    }
    PyArray_ITER_NEXT(it1);
    PyArray_ITER_RESET(it2);
}

在上面的代码中,op 是返回矩阵,dot 是一维点积函数,it1it2 是输入矩阵的迭代器。

话虽如此,看起来您的代码可能已经在做正确的事情了。在这种情况下,最佳性能实际上比 O(n^3/sprt(M)) 好得多,您可以将 IO 限制为仅从磁盘读取 A 的每个项目,或 O(n)。 Memmap 数组自然必须在后台进行一些缓存,并且内部循环在 it2 上运行,因此如果 A 是 C 顺序并且 memmap 缓存足够大,那么您的代码可能已经在工作了。您可以通过执行以下操作显式地强制缓存 A 的行:

def my_dot(A, B, C):

    for ii in xrange(n):
        A_ii = np.array(A[ii, :])
        C[ii, :] = A_ii.dot(B)

    return C

【讨论】:

  • 这令人放心——我想知道其他 linalg 操作在多大程度上倾向于与 memmaped 数组的缓存一起工作。您是否碰巧知道是否可以控制缓存大小?我从来没有找到一个很好的资源来解释缓存和内存使用是如何由 memmap 控制的。
  • 请注意,PyArray_MatrixProduct2 在无法调用 BLAS 的情况下由 np.dot 使用(例如,非 BLAS 兼容的内存顺序、非浮点数据类型、无 BLAS库安装)。见here
  • 基于它使用我的 4 个内核这一事实,np.dot 在将 memmapped float32 数组与非 memmapped float32 数组相乘时似乎确实调用了 BLAS,所以 PyArray_MatrixProduct2 可能不会不会被调用。
【解决方案2】:

我已经实现了一个函数,用于将np.dot 应用于从内存映射数组显式读入核心内存的块:

import numpy as np

def _block_slices(dim_size, block_size):
    """Generator that yields slice objects for indexing into 
    sequential blocks of an array along a particular axis
    """
    count = 0
    while True:
        yield slice(count, count + block_size, 1)
        count += block_size
        if count > dim_size:
            raise StopIteration

def blockwise_dot(A, B, max_elements=int(2**27), out=None):
    """
    Computes the dot product of two matrices in a block-wise fashion. 
    Only blocks of `A` with a maximum size of `max_elements` will be 
    processed simultaneously.
    """

    m,  n = A.shape
    n1, o = B.shape

    if n1 != n:
        raise ValueError('matrices are not aligned')

    if A.flags.f_contiguous:
        # prioritize processing as many columns of A as possible
        max_cols = max(1, max_elements / m)
        max_rows =  max_elements / max_cols

    else:
        # prioritize processing as many rows of A as possible
        max_rows = max(1, max_elements / n)
        max_cols =  max_elements / max_rows

    if out is None:
        out = np.empty((m, o), dtype=np.result_type(A, B))
    elif out.shape != (m, o):
        raise ValueError('output array has incorrect dimensions')

    for mm in _block_slices(m, max_rows):
        out[mm, :] = 0
        for nn in _block_slices(n, max_cols):
            A_block = A[mm, nn].copy()  # copy to force a read
            out[mm, :] += np.dot(A_block, B[nn, :])
            del A_block

    return out

然后我做了一些基准测试,将我的blockwise_dot 函数与直接应用于内存映射数组的普通np.dot 函数进行比较(请参阅下面的基准测试脚本)。我正在使用与 OpenBLAS v0.2.9.rc1 链接的 numpy 1.9.0.dev-205598b(从源代码编译)。该机器是运行 Ubuntu 13.10 的四核笔记本电脑,具有 8GB RAM 和 SSD,我已禁用交换文件。

结果

正如@Bi Rico 预测的那样,相对于A 的维度,计算点积所需的时间非常O(n)。对 A 的缓存块进行操作比仅在整个内存映射数组上调用普通的 np.dot 函数具有巨大的性能提升:

令人惊讶的是,它对正在处理的块的大小不敏感 - 处理 1GB、2GB 或 4GB 块中的数组所花费的时间几乎没有差别。我的结论是,无论缓存 np.memmap 数组本机实现什么,它似乎都不是计算点积的最佳选择。

其他问题

不得不手动实现这种缓存策略仍然有点痛苦,因为我的代码可能必须在具有不同物理内存量和可能不同操作系统的机器上运行。出于这个原因,我仍然对是否有办法控制内存映射数组的缓存行为以提高np.dot 的性能感兴趣。

我在运行基准测试时注意到了一些奇怪的内存处理行为 - 当我在整个 A 上调用 np.dot 时,我从未见过我的 Python 进程的驻留集大小超过大约 3.8GB,即使我有大约 7.5GB 的可用 RAM。这让我怀疑np.memmap 数组允许占用的物理内存量有一些限制——我之前假设它会使用操作系统允许它抓取的任何 RAM。就我而言,能够提高此限制可能非常有益。

是否有人对 np.memmap 数组的缓存行为有任何进一步的了解,有助于解释这一点?

基准测试脚本

def generate_random_mmarray(shape, fp, max_elements):
    A = np.memmap(fp, dtype=np.float32, mode='w+', shape=shape)
    max_rows = max(1, max_elements / shape[1])
    max_cols =  max_elements / max_rows
    for rr in _block_slices(shape[0], max_rows):
        for cc in _block_slices(shape[1], max_cols):
            A[rr, cc] = np.random.randn(*A[rr, cc].shape)
    return A

def run_bench(n_gigabytes=np.array([16]), max_block_gigabytes=6, reps=3,
              fpath='temp_array'):
    """
    time C = A * B, where A is a big (n, n) memory-mapped array, and B and C are
    (n, o) arrays resident in core memory
    """

    standard_times = []
    blockwise_times = []
    differences = []
    nbytes = n_gigabytes * 2 ** 30
    o = 64

    # float32 elements
    max_elements = int((max_block_gigabytes * 2 ** 30) / 4)

    for nb in nbytes:

        # float32 elements
        n = int(np.sqrt(nb / 4))

        with open(fpath, 'w+') as f:
            A = generate_random_mmarray((n, n), f, (max_elements / 2))
            B = np.random.randn(n, o).astype(np.float32)

            print "\n" + "-"*60
            print "A: %s\t(%i bytes)" %(A.shape, A.nbytes)
            print "B: %s\t\t(%i bytes)" %(B.shape, B.nbytes)

            best = np.inf
            for _ in xrange(reps):
                tic = time.time()
                res1 = np.dot(A, B)
                t = time.time() - tic
                best = min(best, t)
            print "Normal dot:\t%imin %.2fsec" %divmod(best, 60)
            standard_times.append(best)

            best = np.inf
            for _ in xrange(reps):
                tic = time.time()
                res2 = blockwise_dot(A, B, max_elements=max_elements)
                t = time.time() - tic
                best = min(best, t)
            print "Block-wise dot:\t%imin %.2fsec" %divmod(best, 60)
            blockwise_times.append(best)

            diff = np.linalg.norm(res1 - res2)
            print "L2 norm of difference:\t%g" %diff
            differences.append(diff)

        del A, B
        del res1, res2
        os.remove(fpath)

    return (np.array(standard_times), np.array(blockwise_times), 
            np.array(differences))

if __name__ == '__main__':
    n = np.logspace(2,5,4,base=2)
    standard_times, blockwise_times, differences = run_bench(
                                                    n_gigabytes=n,
                                                    max_block_gigabytes=4)

    np.savez('bench_results', standard_times=standard_times, 
             blockwise_times=blockwise_times, differences=differences)

【讨论】:

  • 请提交你的系统参数和python\numpy\packages都是x64的吗?
  • @mrgloom 一切都是 x64。我的回答中描述了所有其他相关参数。
  • 您应该可以使用Strassen algorithm 之类的解决方案来减少切片的点积。但这会花费你更多的内存。 (我猜这意味着更小的切片)
  • @Mehdi 很高兴知道以备将来使用。不幸的是,内存消耗是我目前最受限制的。降低的数值稳定性也可能是一个问题。我的猜测是,对于实际的点积,在速度方面很难击败优化的 BLAS 函数。
【解决方案3】:

我建议您使用 PyTables 而不是 numpy.memmap。还阅读了他们关于压缩的介绍,这对我来说听起来很奇怪,但似乎是序列"compress->transfer->uncompress" is faster then just transfer uncompressed

还可以将 np.dot 与 MKL 一起使用。而且我不知道如何将 numexpr(pytables also seems have something like it) 用于矩阵乘法,但例如计算欧几里得范数,它是最快的方法(与 numpy 相比)。

尝试对这个示例代码进行基准测试:

import numpy as np
import tables
import time
n_row=1000
n_col=1000
n_batch=100
def test_hdf5_disk():
    rows = n_row
    cols = n_col
    batches = n_batch
    #settings for all hdf5 files
    atom = tables.Float32Atom()
    filters = tables.Filters(complevel=9, complib='blosc') # tune parameters
    Nchunk = 4*1024  # ?
    chunkshape = (Nchunk, Nchunk)
    chunk_multiple = 1
    block_size = chunk_multiple * Nchunk

    fileName_A = 'carray_A.h5'
    shape_A = (n_row*n_batch, n_col)  # predefined size
    h5f_A = tables.open_file(fileName_A, 'w')
    A = h5f_A.create_carray(h5f_A.root, 'CArray', atom, shape_A, chunkshape=chunkshape, filters=filters)
    for i in range(batches):
        data = np.random.rand(n_row, n_col)
        A[i*n_row:(i+1)*n_row]= data[:]
    rows = n_col
    cols = n_row
    batches = n_batch
    fileName_B = 'carray_B.h5'
    shape_B = (rows, cols*batches)  # predefined size
    h5f_B = tables.open_file(fileName_B, 'w')
    B = h5f_B.create_carray(h5f_B.root, 'CArray', atom, shape_B, chunkshape=chunkshape, filters=filters)
    sz= rows/batches
    for i in range(batches):
        data = np.random.rand(sz, cols*batches)
        B[i*sz:(i+1)*sz]= data[:]
    fileName_C = 'CArray_C.h5'
    shape = (A.shape[0], B.shape[1])
    h5f_C = tables.open_file(fileName_C, 'w')
    C = h5f_C.create_carray(h5f_C.root, 'CArray', atom, shape, chunkshape=chunkshape, filters=filters)
    sz= block_size
    t0= time.time()
    for i in range(0, A.shape[0], sz):
        for j in range(0, B.shape[1], sz):
            for k in range(0, A.shape[1], sz):
                C[i:i+sz,j:j+sz] += np.dot(A[i:i+sz,k:k+sz],B[k:k+sz,j:j+sz])
    print (time.time()-t0)
    h5f_A.close()
    h5f_B.close()
    h5f_C.close()

我不知道如何根据当前机器调整块大小和压缩率的问题,所以我认为性能可能取决于参数。

另外请注意,示例代码中的所有矩阵都存储在磁盘上,如果将其中一些存储在 RAM 中,我认为会更快。

顺便说一句,我使用的是 x32 机器和 numpy.memmap,我对矩阵大小有一些限制(我不确定,但似乎视图大小只能是 ~2Gb)并且 PyTables 没有限制。

【讨论】:

  • 对 PyTables 数组进行操作有点吸引人,部分原因是数据已经存储在 PyTables 数组中。但是,它们比 numpy 数组更难处理。我还必须对A 的转置执行点积,并且由于它们缺少转置方法,这使我的索引变得更加尴尬。最大的问题可能是选择合适的块形状,因为我还必须对 A 的单行/列以及最好在方形块上执行的点积执行操作。
  • PyTables 数组是否会比 memmap 数组更快都取决于我的真实数据的可压缩性,以及我可以节省多少 I/O 带宽。不幸的是,我的本地机器上没有要测试的真实数据集(正如我所说,它们相当大......),但我可以告诉你,我一直在使用的高斯合成数据没有性能使用 PyTables CArrays 而不是 memmaps 的优势。这一点也不奇怪,因为随机数据根据定义是不可压缩的。有机会我会用真实数据做一些基准测试。
  • 在那个线程中,我认为 Anthony Scopatz 假设您的输入数组足够小,可以保存在内存中。当然,在整个数组上调用np.dot 会更快,但我显然不能这样做。
猜你喜欢
  • 1970-01-01
  • 2020-01-25
  • 1970-01-01
  • 2019-09-21
  • 2017-06-15
  • 1970-01-01
  • 1970-01-01
  • 2020-01-31
  • 1970-01-01
相关资源
最近更新 更多