python arrays performance numpy linear-algebra

python - Productos de puntos eficientes de grandes matrices mapeadas en memoria



arrays performance (3)

Estoy trabajando con algunas matrices flotantes numpy densas y bastante grandes que actualmente residen en el disco en PyTables CArray s. Necesito poder realizar productos de puntos eficientes usando estas matrices, por ejemplo C = A.dot(B) , donde A es una matriz mapeada de memoria enorme (~ 1E4 x 3E5 float32), y B y C son matrices numpy más pequeñas que son residentes en la memoria del núcleo.

Lo que estoy haciendo en este momento es copiar los datos en matrices numpy mapeadas en memoria usando np.memmap , y luego llamar a np.dot directamente en las matrices mapeadas en memoria. Esto funciona, pero sospecho que el np.dot estándar (o más bien las funciones BLAS subyacentes que llama) probablemente no sea muy eficiente en términos del número de operaciones de E / S requeridas para calcular el resultado.

Encontré un ejemplo interesante en este artículo de revisión . Un producto de puntos ingenuo calculado utilizando 3x bucles anidados, como este:

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

requiere O (n ^ 3) operaciones de E / S para calcular.

Sin embargo, procesando las matrices en bloques de tamaño apropiado:

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

donde M es la cantidad máxima de elementos que cabrán en la memoria del núcleo, el número de operaciones de E / S se reduce a O (n ^ 3 / sqrt (M)) .

¿Qué tan inteligente es np.dot y / o np.memmap ? ¿ np.dot a np.dot realizar un producto de punto de bloqueo eficiente de E / S? ¿ np.memmap hace algún caché elegante que mejore la eficiencia de este tipo de operación?

Si no es así, ¿existe alguna función de biblioteca preexistente que realice productos de E / S eficientes, o debería intentar implementarla yo mismo?

Actualizar

Hice algunas evaluaciones comparativas con una implementación np.dot de np.dot que opera en bloques de la matriz de entrada, que se leen explícitamente en la memoria del núcleo. Esta información al menos parcialmente responde a mi pregunta original, así que la estoy publicando como respuesta.


Implementé una función para aplicar np.dot a bloques que se leen explícitamente en la memoria del núcleo de la matriz mapeada en memoria:

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

Luego hice algunos benchmarking para comparar mi función blockwise_dot con la función np.dot normal aplicada directamente a una matriz mapeada en memoria (ver a continuación el script de benchmarking). Estoy usando numpy 1.9.0.dev-205598b vinculado con OpenBLAS v0.2.9.rc1 (compilado de la fuente). La máquina es una computadora portátil de cuatro núcleos con Ubuntu 13.10, con 8 GB de RAM y una SSD, y he desactivado el archivo de intercambio.

Resultados

Como predijo @Bi Rico, el tiempo necesario para calcular el producto escalar es maravillosamente O (n) con respecto a las dimensiones de A Operar en bloques en caché de A brinda una gran mejora en el rendimiento sobre la simple invocación de la función np.dot normal en toda la matriz mapeada en memoria:

Es sorprendentemente insensible al tamaño de los bloques que se procesan: hay muy poca diferencia entre el tiempo necesario para procesar la matriz en bloques de 1 GB, 2 GB o 4 GB. Concluyo que cualquiera que sea el cache de np.memmap matrices de np.memmap implementadas de forma nativa, parece ser muy poco óptimo para el cálculo de productos de puntos.

Mas preguntas

Todavía es un poco doloroso tener que implementar manualmente esta estrategia de almacenamiento en caché, ya que es probable que mi código tenga que ejecutarse en máquinas con diferentes cantidades de memoria física y sistemas operativos potencialmente diferentes. Por esa razón, todavía estoy interesado en si hay formas de controlar el comportamiento de almacenamiento en caché de las matrices mapeadas en memoria para mejorar el rendimiento de np.dot .

np.dot comportamiento de manejo de memoria extraño mientras ejecutaba los puntos de referencia: cuando llamé a np.dot en todo A , nunca vi que el tamaño del conjunto residente de mi proceso de Python superara los 3.8 GB, a pesar de que tengo aproximadamente 7.5 GB de RAM gratis. Esto me lleva a sospechar que existe un límite impuesto sobre la cantidad de memoria física que puede ocupar una matriz np.memmap : anteriormente había supuesto que usaría la RAM que el sistema operativo le permitiera capturar. En mi caso, podría ser muy beneficioso poder aumentar este límite.

¿Alguien tiene más información sobre el comportamiento de almacenamiento en caché de matrices np.memmap que ayudaría a explicar esto?

Guión de Benchmarking

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)


No creo que numpy optimice el producto de puntos para las matrices de memmap, si miras el código de multiplicación de matrices, que obtuve here , verás que la función MatrixProduct2 (como se implementa actualmente) calcula los valores de la matriz de resultados en c orden de memoria:

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); }

En el código anterior, op es la matriz de retorno, dot es la función de producto de 1d punto e it1 e it2 son iteradores sobre las matrices de entrada.

Dicho esto, parece que su código ya está haciendo lo correcto. En este caso, el rendimiento óptimo es en realidad mucho mejor que O (n ^ 3 / sprt (M)), puede limitar su IO a solo leer cada elemento de A una vez desde el disco, o O (n). Los arreglos de Memmap naturalmente tienen que hacer algo de almacenamiento en caché detrás de la escena y el bucle interno opera en it2 , por lo que si A está en orden C y el caché de memmap es lo suficientemente grande, es posible que su código ya esté funcionando. Puede aplicar el almacenamiento en caché de filas de A de forma explícita haciendo algo como:

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


Te recomiendo que uses PyTables en lugar de numpy.memmap. También leo sus presentaciones sobre la compresión, me parece extraño, pero parece que la secuencia "comprimir-> transferir-> descomprimir" es más rápida que simplemente transferir sin comprimir .

También use np.dot con MKL. Y no sé cómo el numexpr (las tablas también parecen tener algo así ) se puede usar para la multiplicación de matrices, pero por ejemplo para calcular la norma euclidiana es la forma más rápida (en comparación con el numpy).

Intente comparar este código de muestra:

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

El problema es que no sé cómo ajustar el tamaño del fragmento y la tasa de compresión a la máquina actual, por lo que creo que el rendimiento puede depender de los parámetros.

También tenga en cuenta que todas las matrices en el código de muestra se almacenan en el disco, si algunos de ellos se almacenan en la memoria RAM, creo que será más rápido.

Por cierto estoy usando la máquina x32 y con numpy.memmap tengo algunas limitaciones en el tamaño de la matriz (no estoy seguro, pero parece que el tamaño de la vista puede ser solo ~ 2Gb) y las PyTables no tienen limitaciones.