web-dev-qa-db-fra.com

Comment paralléliser ceci Python pour la boucle lors de l'utilisation de Numba

J'utilise la distribution Anaconda de Python, avec Numba, et j'ai écrit la fonction Python suivante qui multiplie une matrice clairsemée A (stocké au format CSR) par un vecteur dense x:

@jit
def csrMult( x, Adata, Aindices, Aindptr, Ashape ):

    numRowsA = Ashape[0]
    Ax       = numpy.zeros( numRowsA )

    for i in range( numRowsA ):
        Ax_i = 0.0
        for dataIdx in range( Aindptr[i], Aindptr[i+1] ):

            j     = Aindices[dataIdx]
            Ax_i +=    Adata[dataIdx] * x[j]

        Ax[i] = Ax_i

    return Ax 

Voici A est une grande scipy matrice clairsemée,

>>> A.shape
( 56469, 39279 )
#                  having ~ 142,258,302 nonzero entries (so about 6.4% )
>>> type( A[0,0] )
dtype( 'float32' )

et x est un tableau numpy. Voici un extrait de code qui appelle la fonction ci-dessus:

x       = numpy.random.randn( A.shape[1] )
Ax      = A.dot( x )   
AxCheck = csrMult( x, A.data, A.indices, A.indptr, A.shape )

Remarquez le @jit - décorateur qui dit à Numba de faire une compilation juste à temps pour le csrMult() fonction.

Dans mes expériences, ma fonction csrMult() est environ deux fois plus rapide que la scipy.dot() méthode. C'est un résultat assez impressionnant pour Numba.

Cependant, MATLAB effectue toujours cette multiplication matrice-vecteur environ 6 fois plus rapidement que csrMult(). Je pense que c'est parce que MATLAB utilise le multithreading lors de l'exécution d'une multiplication matricielle-vecteur clairsemée.


Question:

Comment puis-je paralléliser la boucle externe for- lors de l'utilisation de Numba?

Numba avait une fonction prange(), qui rendait simple la parallélisation embarrassante parallèle for - boucles. Malheureusement, Numba n'a plus de prange() [ en fait, c'est faux, voir l'édition ci-dessous ]. Alors, quelle est la bonne façon de paralléliser cette boucle for- maintenant, que la fonction prange() de Numba a disparu?

Lorsque prange() a été supprimé de Numba, quelle alternative les développeurs de Numba avaient-ils en tête?


Modifier 1:
J'ai mis à jour la dernière version de Numba, qui est .35, et prange() est de retour! Il n'était pas inclus dans la version .33, la version que j'utilisais.
C'est une bonne nouvelle, mais je reçois malheureusement un message d'erreur lorsque j'essaie de paralléliser ma boucle for à l'aide de prange(). Voici un parallèle pour la boucle exemple de la documentation Numba (voir la section 1.9.2 "Boucles parallèles explicites"), et ci-dessous mon nouveau code:

from numba import njit, prange
@njit( parallel=True )
def csrMult_numba( x, Adata, Aindices, Aindptr, Ashape):

    numRowsA = Ashape[0]    
    Ax       = np.zeros( numRowsA )

    for i in prange( numRowsA ):
        Ax_i = 0.0        
        for dataIdx in range( Aindptr[i],Aindptr[i+1] ):

            j     = Aindices[dataIdx]
            Ax_i +=    Adata[dataIdx] * x[j]

        Ax[i] = Ax_i            

    return Ax 

Lorsque j'appelle cette fonction, à l'aide de l'extrait de code ci-dessus, je reçois l'erreur suivante:

AttributeError: échec à nopython (conversion en parfors) L'objet 'SetItem' n'a pas d'attribut 'get_targets'


Donné
la tentative ci-dessus d'utiliser prange plante, ma question est:

Quelle est la bonne façon (en utilisant prange ou une méthode alternative) pour paralléliser ce Python for- boucle?

Comme indiqué ci-dessous, il était trivial de paralléliser une boucle for similaire en C++ et d'obtenir une accélération 8x , ayant été exécuté sur 20 - omp-threads. Il doit y avoir un moyen de le faire en utilisant Numba, car la boucle for est embarrassamment parallèle (et puisque la multiplication matrice-vecteur éparse est une opération fondamentale dans le calcul scientifique).


Édition 2:
Voici ma version C++ de csrMult(). La parallélisation de la boucle for() dans la version C++ rend le code environ 8 fois plus rapide dans mes tests. Cela me suggère qu'une accélération similaire devrait être possible pour la version Python lors de l'utilisation de Numba.

void csrMult(VectorXd& Ax, VectorXd& x, vector<double>& Adata, vector<int>& Aindices, vector<int>& Aindptr)
{
    // This code assumes that the size of Ax is numRowsA.
    #pragma omp parallel num_threads(20)
    {       
        #pragma omp for schedule(dynamic,590) 
        for (int i = 0; i < Ax.size(); i++)
        {
            double Ax_i = 0.0;
            for (int dataIdx = Aindptr[i]; dataIdx < Aindptr[i + 1]; dataIdx++)
            {
                Ax_i += Adata[dataIdx] * x[Aindices[dataIdx]];
            }

            Ax[i] = Ax_i;
        }
    }
}
15
littleO

Merci pour vos mises à jour quantitatives, Daniel.
Les lignes suivantes peuvent être difficiles à avaler, mais croyez-moi, il y a plus de choses à prendre en compte. J'ai travaillé sur des problèmes de calcul parallèle/HPC ayant des matrices dans des échelles ~ N [TB]; N > 10 et leurs accompagnements clairsemés, donc certaines expériences peuvent être utile pour vos vues ultérieures.

ATTENTION: Ne vous attendez pas à ce qu'un dîner soit servi gratuitement

Une volonté de paralléliser un morceau de code sonne comme un mana ré-articulé de plus en plus souvent contemporain. Le problème n'est pas pas le code, mais le coût d'un tel mouvement.

L'économie est le problème numéro un. La loi d'Amdahl, telle qu'elle a été formulée à l'origine par Gene Amdahl, ne tenait pas compte des coûts mêmes de [PAR] - process-setups + [PAR] - process-finalisations & terminations, qui doivent en effet être payé dans chaque mise en œuvre du monde réel.

La loi d'Amdahl strictement stricte décrit l'ampleur de ces effets indésirables inévitables et aide à comprendre quelques nouveaux aspects qui doivent être évalués avant de choisir d'introduire la parallélisation (à un coût acceptable, car il est très, en effet TRÈS FACILE de payer BEAUCOUP plus que ce que l'on peut gagner - où une déception naïve d'une performance de traitement dégradée est la partie la plus facile de l'histoire ).

N'hésitez pas à lire d'autres articles sur la reformulation de la loi d'Amdahl stricte, si vous souhaitez mieux comprendre ce sujet et pré-calculer le réel " minimum" - subProblem - " size" , pour lequel le somme des - [PAR] - les frais généraux seront au moins justifiés à partir d'outils du monde réel pour introduire la division parallèle du sous-problème sur N_trully_[PAR]_processes (pas n'importe quel "juste" - [CONCURRENT], Mais vrai - [PARALLEL] - ce sont des moyens différents).


Python peut recevoir une dose de stéroïdes pour des performances accrues:

Python est un excellent écosystème de prototypage, tandis que numba, numpy et d'autres extensions compilées aident beaucoup à augmenter les performances bien plus loin qu'un natif, à pas GIL python (co -)), le traitement fournit généralement.

Ici, vous essayez d'appliquer numba.jit() pour organiser le travail presque - gratuitement, juste par son jit() - analyseur lexical temporel automatisé (sur lequel vous lancez votre code), qui devrait à la fois "comprendre" votre objectif global ( Quoi à faire), et propose également quelques astuces de vectorisation ( Comment mieux assembler un tas d'instructions CPU pour une efficacité maximale d'une telle exécution de code) .

Cela semble facile, mais ce n'est pas le cas.

L'équipe de Travis Oliphant a fait d'immenses progrès sur les outils numba, mais soyons réalistes et équitables pour ne pas attendre une quelconque forme de sorcellerie automatisée être implémenté dans une analyse de code .jit() - lexer +, lorsque vous essayez de transformer un code et d'assembler un flux d'instructions machine plus efficace pour implémenter l'objectif de la tâche de haut niveau.

@guvectorize? Ici? Sérieusement?

En raison du dimensionnement de [PSPACE], Vous pouvez immédiatement oublier de demander à numba de "bourrer" efficacement le moteur GPU de données, dont l'empreinte mémoire est bien derrière les tailles GPU-GDDR ( ne parle pas du tout de tailles de noyau GPU trop "superficielles" pour un tel traitement mathématiquement "minuscule" pour simplement multiplier, potentiellement dans [PAR], mais pour additionner plus tard dans [SEQ]).

(Re -) - Le chargement du GPU avec des données prend beaucoup de temps. Si vous avez payé cela, les latences de mémoire In-GPU ne sont pas très conviviales pour l'économie "minuscule" -GPU-kernels - votre exécution de code GPU-SMX devra payer ~ 350-700 [ns] Juste pour récupérer un nombre (probablement pas automatiquement réaligné pour la meilleure réutilisation compatible avec le cache SM dans les prochaines étapes et vous remarquerez peut-être que vous ne , permettez-moi de le répéter, ne réutilisez JAMAIS une seule cellule de matrice, donc la mise en cache en soi ne fournira rien sous ces 350~700 [ns] par cellule de matrice), pendant qu'un smart pure numpy- le code vectorisé peut traiter le produit matrice-vecteur en moins de 1 [ns] par cellule, même sur les plus grandes [PSPACE] - empreintes .

C'est un critère de comparaison.

(Le profilage ferait mieux de montrer ici les faits, mais le principe est bien connu à l'avance, sans tester comment déplacer quelques TB de données sur le GPU-fabric juste pour s'en rendre compte par soi-même.)


La pire des mauvaises nouvelles:

Etant donné les échelles de mémoire de la matrice A, le pire effet à attendre est que l’organisation clairsemée du stockage du la représentation matricielle dévastera probablement la plupart, sinon la totalité, des gains de performances possibles réalisables par numba- astuces vectorisées sur les représentations matricielles denses, car il n'y aura probablement presque aucune chance de réutilisation efficace de la ligne de cache récupérée en mémoire et la rareté brisera également tout moyen facile de réaliser une cartographie compacte des opérations vectorisées et celles-ci ne pourront guère être facilement traduites en ressources de traitement vectoriel matériel-CPU avancées.


Inventaire des problèmes résolubles:

  • toujours mieux pré-allouer le vecteur Ax = np.zeros_like( A[:,0] ) et le passer comme autre paramètre dans le numba.jit() - parties compilées du code, afin d’éviter des paiements répétitifs supplémentaires [PTIME,PSPACE] - coûts pour créer (encore) de nouvelles allocations de mémoire (d'autant plus si le vecteur est suspecté d'être utilisé dans un processus d'optimisation itératif orchestré en externe)
  • toujours mieux spécifier (pour réduire l'universalité, pour des raisons de performances du code résultant)
    au moins les directives d'interface appelant numba.jit( "f8[:]( f4[:], f4[:,:], ... )" )
  • examine toujours toutes les options numba.jit() - disponibles et leurs valeurs par défaut respectives (peut changer de version en version) pour votre situation spécifique (désactiver GIL et mieux aligner les objectifs avec les capacités matérielles numba + aidera toujours dans les parties du code à forte intensité numérique)

@jit(   signature = [    numba.float32( numba.float32, numba.int32 ),                                   #          # [_v41] @decorator with a list of calling-signatures
                         numba.float64( numba.float64, numba.int64 )                                    #
                         ],    #__________________ a list of signatures for prepared alternative code-paths, to avoid a deferred lazy-compilation if undefined
        nopython = False,      #__________________ forces the function to be compiled in nopython mode. If not possible, compilation will raise an error.
        nogil    = False,      #__________________ tries to release the global interpreter lock inside the compiled function. The GIL will only be released if Numba can compile the function in nopython mode, otherwise a compilation warning will be printed.
        cache    = False,      #__________________ enables a file-based cache to shorten compilation times when the function was already compiled in a previous invocation. The cache is maintained in the __pycache__ subdirectory of the directory containing the source file.
        forceobj = False,      #__________________ forces the function to be compiled in object mode. Since object mode is slower than nopython mode, this is mostly useful for testing purposes.
        locals   = {}          #__________________ a mapping of local variable names to Numba Types.
        ) #____________________# [_v41] ZERO <____ TEST *ALL* CALLED sub-func()-s to @.jit() too >>>>>>>>>>>>>>>>>>>>> [DONE]
 def r...(...):
      ...
5
user3666197

Numba a été mis à jour et prange() fonctionne maintenant!  (Je réponds à ma propre question.)

Les améliorations des capacités de calcul parallèle de Numba sont discutées dans ce article de blog , daté du 12 décembre 2017. Voici un extrait pertinent du blog:

Il y a longtemps (plus de 20 versions!), Numba avait le support d'un idiome pour écrire en parallèle pour les boucles appelées prange(). Après une refactorisation majeure de la base de code en 2014, cette fonctionnalité a dû être supprimée, mais elle est depuis l'une des fonctionnalités Numba les plus demandées. Après que les développeurs d'Intel aient parallélisé les expressions de tableau, ils ont réalisé que ramener prange serait assez facile

En utilisant la version 0.36.1 de Numba, je peux paralléliser ma boucle for- parallèle embarrassante en utilisant le code simple suivant:

@numba.jit(nopython=True, parallel=True)
def csrMult_parallel(x,Adata,Aindices,Aindptr,Ashape): 

    numRowsA = Ashape[0]    
    Ax = np.zeros(numRowsA)

    for i in numba.prange(numRowsA):
        Ax_i = 0.0        
        for dataIdx in range(Aindptr[i],Aindptr[i+1]):

            j = Aindices[dataIdx]
            Ax_i += Adata[dataIdx]*x[j]

        Ax[i] = Ax_i            

    return Ax

Dans mes expériences, la parallélisation de la boucle for- a fait exécuter la fonction environ huit fois plus vite que la version que j'ai publiée au début de ma question, qui utilisait déjà Numba, mais qui n'était pas parallélisée. De plus, dans mes expériences, la version parallélisée est environ 5 fois plus rapide que la commande Ax = A.dot(x) qui utilise la fonction de multiplication matricielle-vecteur éparse de scipy. Numba a écrasé scipy et j'ai enfin une python routine de multiplication matricielle-vecteur éparse qui est aussi vite que MATLAB .

8
littleO