34 votes

Produits scalaires efficaces de grands tableaux à mémoire partagée

Je travaille avec des tableaux de flotteurs numpy assez grands et denses qui résident actuellement sur le disque dans PyTables. CArray s. Je dois être en mesure d'effectuer des produits scalaires efficaces en utilisant ces tableaux, par exemple C = A.dot(B) , donde A est un énorme (~1E4 x 3E5 float32) tableau mappé en mémoire, et B y C sont des tableaux numpy plus petits qui résident dans la mémoire centrale.

Ce que je fais pour le moment, c'est copier les données dans des tableaux numpy mappés en mémoire en utilisant np.memmap puis en appelant np.dot directement sur les tableaux de mémoire. Cela fonctionne, mais je soupçonne que la norme np.dot (ou plutôt les fonctions BLAS sous-jacentes qu'il appelle) n'est probablement pas très efficace en termes de nombre d'opérations d'E/S nécessaires pour calculer le résultat.

Je suis tombé sur un exemple intéressant dans cet article de synthèse . Un produit scalaire naïf calculé en utilisant 3x boucles imbriquées, comme ceci :

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

nécessite O(n^3) Opérations d'E/S à calculer.

Cependant, en traitant les tableaux en blocs de taille appropriée :

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 est le nombre maximal d'éléments pouvant tenir dans la mémoire centrale, le nombre d'opérations d'E/S est réduit à O(n^3 / sqrt(M)) .

A quel point np.dot et/ou np.memmap ? Est-ce que le fait d'appeler np.dot effectuer un produit scalaire par blocs efficace en termes d'E/S ? Est-ce que np.memmap faire une mise en cache fantaisiste qui améliorerait l'efficacité de ce type d'opération ?

Si ce n'est pas le cas, existe-t-il une fonction de bibliothèque préexistante qui réalise des produits scalaires efficaces en termes d'E/S, ou dois-je essayer de l'implémenter moi-même ?

Mise à jour

J'ai fait quelques tests de référence avec une implémentation manuelle de np.dot qui opère sur des blocs du tableau d'entrée, qui sont explicitement lus dans la mémoire centrale. Ces données répondent au moins partiellement à ma question initiale, je les publie donc comme réponse.

24voto

ali_m Points 7185

J'ai implémenté une fonction pour appliquer np.dot aux blocs qui sont explicitement lus dans la mémoire centrale à partir de la matrice mappée en mémoire :

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

J'ai ensuite procédé à une analyse comparative de mes blockwise_dot à la fonction normale np.dot appliquée directement à un tableau mappé en mémoire (voir ci-dessous pour le script d'évaluation comparative). J'utilise numpy 1.9.0.dev-205598b lié à OpenBLAS v0.2.9.rc1 (compilé à partir des sources). La machine est un ordinateur portable quad-core fonctionnant sous Ubuntu 13.10, avec 8 Go de RAM et un SSD, et j'ai désactivé le fichier swap.

Résultats

Comme l'a prédit @Bi Rico, le temps nécessaire pour calculer le produit scalaire est magnifique. O(n) par rapport aux dimensions de A . Fonctionnement sur des blocs en cache de A permet d'améliorer considérablement les performances par rapport à l'appel de la fonction normale np.dot sur l'ensemble du tableau de la mémoire :

enter image description here

Il est étonnamment insensible à la taille des blocs traités - il y a très peu de différence entre le temps pris pour traiter le tableau en blocs de 1 Go, 2 Go ou 4 Go. J'en conclus que quelle que soit la mise en cache np.memmap que les tableaux implémentent nativement, il semble être très sous-optimal pour le calcul des produits scalaires.

Autres questions

C'est toujours un peu pénible de devoir mettre en œuvre manuellement cette stratégie de mise en cache, puisque mon code devra probablement être exécuté sur des machines ayant des quantités différentes de mémoire physique, et potentiellement des systèmes d'exploitation différents. Pour cette raison, je suis toujours intéressé de savoir s'il existe des moyens de contrôler le comportement de la mise en cache des tableaux mappés en mémoire afin d'améliorer les performances des applications suivantes np.dot .

J'ai remarqué un comportement étrange en matière de gestion de la mémoire lors de l'exécution des tests de référence. np.dot sur l'ensemble de A Je n'ai jamais vu la taille de l'ensemble résident de mon processus Python dépasser environ 3,8 Go, même si j'ai environ 7,5 Go de RAM libre. Cela m'amène à penser qu'il existe une limite imposée à la quantité de mémoire physique qu'un processus Python peut utiliser. np.memmap J'avais précédemment supposé qu'il utiliserait toute la RAM que le système d'exploitation lui permettrait d'utiliser. Dans mon cas, il pourrait être très bénéfique de pouvoir augmenter cette limite.

Est-ce que quelqu'un a des informations supplémentaires sur le comportement de la mise en cache de np.memmap des tableaux qui permettraient d'expliquer cela ?

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

6voto

Bi Rico Points 9851

Je ne pense pas que numpy optimise le produit scalaire pour les tableaux memmap, si vous regardez le code pour la multiplication de matrice, que j'ai obtenu aquí vous verrez que la fonction MatrixProduct2 (tel qu'il est actuellement implémenté) calcule les valeurs de la matrice de résultat dans l'ordre de la mémoire 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);
}

Dans le code ci-dessus, op est la matrice de retour, dot est la fonction produit scalaire 1d et it1 y it2 sont des itérateurs sur les matrices d'entrée.

Ceci étant dit, il semble que votre code fasse déjà ce qu'il faut. Dans ce cas, la performance optimale est en fait bien meilleure que O(n^3/sprt(M)), vous pouvez limiter votre IO à la lecture de chaque élément de A une seule fois sur le disque, ou O(n). Les tableaux Memmap doivent naturellement faire de la mise en cache en arrière-plan et la boucle interne opère sur les éléments suivants it2 Si A est dans l'ordre C et que le cache memmap est assez grand, votre code peut déjà fonctionner. Vous pouvez renforcer la mise en cache des rangées de A explicitement en faisant quelque chose comme :

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

5voto

mrgloom Points 1026

Je vous recommande d'utiliser PyTables au lieu de numpy.memmap. Lisez également leurs présentations sur la compression, cela me semble étrange mais il semble que cette séquence "compresser->transférer->décompresser" est plus rapide que de transférer sans compression. .

Utilisez également np.dot avec MKL. Et je ne sais pas comment numexpr( pytables semble aussi avoir quelque chose comme ça ) peut être utilisé pour la multiplication de matrices, mais par exemple pour le calcul de la norme euclidienne, c'est le moyen le plus rapide (en comparaison avec numpy).

Essayez d'évaluer cet exemple de code :

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

Le problème est que je ne sais pas comment ajuster la taille des morceaux et le taux de compression à la machine actuelle, donc je pense que les performances peuvent dépendre des paramètres.

Veuillez également noter que toutes les matrices dans le code d'exemple sont stockées sur le disque, si certaines d'entre elles sont stockées dans la RAM, je pense que ce sera plus rapide.

Au fait, j'utilise une machine x32 et avec numpy.memmap j'ai quelques limitations sur la taille de la matrice (je ne suis pas sûr mais il semble que la taille de la vue ne peut être que de ~2Gb) et PyTables n'a aucune limitation.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X