Подтвердить что ты не робот

Эффективные точечные продукты больших массивов с памятью

Я работаю с довольно большими плотными массивами с плавающей запятой, которые в настоящее время находятся на диске в PyTables CArray s. Мне нужно иметь возможность выполнять эффективные точечные продукты с использованием этих массивов, например C = A.dot(B), где A - это огромный (~ 1E4 x 3E5 float32) массив с памятью, а B и C меньше numpy массивы, которые находятся в основной памяти.

То, что я делаю в данный момент, - это копирование данных в массивы numpy с отображением памяти с использованием np.memmap, а затем вызов np.dot непосредственно на массивы с отображением памяти. Это работает, но я подозреваю, что стандартный np.dot (или, скорее, базовые функции BLAS, которые он вызывает), вероятно, не очень эффективен с точки зрения количества операций ввода-вывода, необходимых для вычисления результата.

Я нашел интересный пример в этой обзорной статье. Наивный точечный продукт, рассчитанный с использованием 3х вложенных циклов, например:

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).

Однако, обрабатывая массивы в блоках соответствующего размера:

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 - максимальное количество элементов, которые будут вписываться в память ядра, количество операций ввода-вывода сводится к O (n ^ 3/sqrt (M)).

Насколько разумным является np.dot и/или np.memmap? Вызывает ли вызов np.dot эффективный продукт с блочной точкой? Имеет ли np.memmap какое-либо причудливое кэширование, которое улучшило бы эффективность такого типа операций?

Если нет, существует ли какая-то ранее существовавшая функция библиотеки, которая выполняет эффективные точечные продукты ввода-вывода, или я должен сам попытаться реализовать ее?

Update

Я провела сравнительный анализ с ручным внедрением np.dot, который работает с блоками входного массива, которые явно считываются в память ядра. Эти данные по крайней мере частично касаются моего первоначального вопроса, поэтому я отправляю его как ответ.

4b9b3361

Ответ 1

Я реализовал функцию для применения 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, примененной непосредственно к массиву с отображением памяти (см. ниже для бенчмаркинга script). Я использую numpy 1.9.0.dev-205598b, связанный с OpenBLAS v0.2.9.rc1 (скомпилирован из источника). Машина представляет собой четырехъядерный ноутбук под управлением Ubuntu 13.10 с 8 ГБ оперативной памяти и SSD, и я отключил файл подкачки.

Результаты

Как предсказал @Bi Rico, время, затраченное на вычисление точечного произведения, красиво O (n) относительно размеров A. Работа с кэшированными блоками A дает огромное улучшение производительности только при вызове нормальной np.dot функции во всем массиве с отображением памяти:

enter image description here

Это удивительно нечувствительно к размеру обрабатываемых блоков - очень мало различий между временем, затрачиваемым на обработку массива в блоках 1 ГБ, 2 ГБ или 4 ГБ. Я пришел к выводу, что все кэширование массивов np.memmap изначально реализуется, оно кажется очень субоптимальным для вычисления точечных продуктов.

Дополнительные вопросы

По-прежнему очень сложно вручную реализовать эту стратегию кэширования, поскольку мой код, вероятно, придется запускать на компьютерах с разным объемом физической памяти и потенциально разных операционных системах. По этой причине меня все еще интересует, существуют ли способы управления кэшированием поведения массивов с отображением памяти, чтобы повысить производительность np.dot.

Я заметил некоторое нечетное поведение обработки памяти, когда я запускал тесты - когда я вызывал np.dot во всем A, я никогда не видел, чтобы размер резидентного набора моего процесса Python превышал примерно 3,8 ГБ, хотя у меня есть около 7,5 ГБ оперативной памяти. Это заставляет меня подозревать, что существует ограничение на количество физической памяти, которой может занимать массив np.memmap - я ранее предполагал, что он будет использовать любую оперативную память, которую ОС позволяет ей захватывать. В моем случае было бы очень полезно увеличить этот предел.

Есть ли у кого-нибудь дальнейшее понимание поведения кеширования массивов np.memmap, которые помогут объяснить это?

Бенчмаркинг script

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)

Ответ 2

Я не думаю, что numpy оптимизирует dot-продукт для массивов memmap, если вы посмотрите на код для умножения матрицы, который я получил здесь, вы увидите, что функция 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 является 1d-точной функцией продукта, а it1 и it2 являются итераторами по входным матрицам.

Говоря это, похоже, что ваш код уже может поступать правильно. В этом случае оптимальная производительность на самом деле намного лучше, чем 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

Ответ 3

Я рекомендую вам использовать PyTables вместо numpy.memmap. Также читайте их презентации о сжатии, это звучит странно для меня, но кажется, что последовательность "compress- > transfer- > uncompress" быстрее, чем просто перенос несжатого.

Также используйте np.dot с MKL. И я не знаю, как numexpr (pytables также, похоже, что-то вроде этого) может использоваться для умножения матрицы, но, например, для вычисления евклидовой нормы это самый быстрый способ (по сравнению с 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()

Проблема в том, что я не знаю, как настроить размер и степень сжатия для текущей машины, поэтому я думаю, что производительность может зависеть от параметров.

Также обратите внимание, что все матрицы в образце кода хранятся на диске, если некоторые из них будут сохранены в ОЗУ, я думаю, что это будет быстрее.

Кстати, я использую машину x32 и с numpy.memmap у меня есть некоторые ограничения на размер матрицы (я не уверен, но кажется, что размер представления может быть только ~ 2Gb), а PyTables не имеет ограничений.