Partage
  • Partager sur Facebook
  • Partager sur Twitter

Problème avec Numba et le multithreading

    25 juillet 2019 à 18:23:23

    Bonjour, 

    J'ai codé un programme de deux façon différentes: une façon sans multithreading, et une façon avec Numba qui fait du multithreading. Le but est de faire une fonction qui permet de renvoyer le résultat et qui en fonction d'un paramètre booléen (que j'ai appelé "Numba") utilise ou non le multithreading.

    J'ai testé le bout de code avec Numba, il stocke bien le résultat que je veux dans "res"... Pourtant, dès que je met le code dans une fonction et que j'essaye de faire return(res), j'obtiens une variable de type non reconnu par python. 

    Voici le code auquel je fais allusion:  (PS: j'ai allégé le code en mettant des valeurs fantaisistes dans les inputs, mais normalement je vais chercher les données dans un tableau excel en local)

    import math
    import threading
    from timeit import repeat
    import numpy as np
    from numba import jit
    import pandas as pd
    import matplotlib.pyplot as plt 
    import xlrd
    import time
    from datetime import date
    from datetime import datetime
    
    #Paramètres du modèle
    
    a=0.1
    sigma=0.001
    nTimes=1000  
    filtre_aberration=0.1
    nthreads = 4
    
    size = 1000
    
    taux_ZC=[2.334,7.8223,3.443]
    dates_ZC=[3.999,5.04,7.4839]
    
    
    #Calcul de la courbe de taux forward instantannés au temps t=0  à partir de la courbe zero-coupon avec la fonction suivante
        
    def taux_forward_instant(R,dates):
        dR_dt=deriv(R,dates)  
        return(R+dates*dR_dt)
            
    def deriv(valeurs,temps):
        return(np.gradient(valeurs,temps))
    
    
    def linear(valeurs,dates,t1,t2,n):
        dates_lin=np.linspace(t1,t2,n)
        valeurs_lin=np.interp(dates_lin, dates, valeurs)
        return([dates_lin,valeurs_lin])
        
    
    @jit('void(double[:,:], double[:], double[:], int32[:], double[:,:], double[:], double[:,:], double[:,:])', nopython=True, nogil=True)
    def inner_func_nb(result, a, sigma, nTimes, dates_lin, r0, theta, dz):
        """
        Function under test.
        """
        for i in range(len(result)):
            result[i][0] = r0[i]
            for k in range(1,nTimes[i]):
                result[i][k]=result[i][k-1]+dz[i][k]+a[i]*(theta[i][k]/a[i]-result[i][k-1])*dates_lin[i][1]
    
    
    
    def timefunc(correct, s, func, *args, **kwargs):
        """
        Benchmark *func* and print out its runtime.
        """
        print(s.ljust(20), end=" ")
        # Make sure the function is compiled before we start the benchmark
        res = func(*args, **kwargs)
        if correct is not None:
            assert np.allclose(res, correct), (res, correct)
        # time it
        print('{:>5.0f} ms'.format(min(repeat(lambda: func(*args, **kwargs),
                                              number=5, repeat=2)) * 1000))
        return res
    
    def make_multithread(inner_func, numthreads, nTimes):
        """
        Run the given function inside *numthreads* threads, splitting its
        arguments into equal-sized chunks.
        """
        def func_mt(*args):
            length = len(args[0])
            result = np.empty((length,nTimes), dtype=np.float64)
            args = (result,) + args
            chunklen = (length + numthreads - 1) // numthreads
            # Create argument tuples for each input chunk
            chunks = [[arg[i * chunklen:(i + 1) * chunklen] for arg in args]
                      for i in range(numthreads)]
            # Spawn one thread per chunk
            threads = [threading.Thread(target=inner_func, args=chunk)
                       for chunk in chunks]
            for thread in threads:
                thread.start()
            for thread in threads:
                thread.join()
            return result
        return func_mt
    
    
    func_nb_mt = make_multithread(inner_func_nb, nthreads, nTimes)
    
    a = np.array([0.1 for k in range(size)])
    sigma = np.array([0.001 for k in range(size)])
    nTimes=np.array([1000 for k in range(size)] )
    dates_lin=np.array([list(np.linspace(dates_ZC[0],dates_ZC[-1],nTimes[0])) for j in range(size)])
    F2=taux_forward_instant(taux_ZC,dates_ZC)
    dF2=deriv(F2,dates_ZC)
    F=linear(F2,dates_ZC,dates_ZC[0],dates_ZC[-1],nTimes[0])[1]
    dF=linear(dF2,dates_ZC,dates_ZC[0],dates_ZC[-1],nTimes[0])[1]
    theta=dF+a[0]*F+((sigma[0]**2)/(2*a[0]))*(1-np.exp(-2*a[0]*dates_lin))
    F_tab=np.array([F for k in range(size)])
    dF_tab=np.array([dF for k in range(size)])
    r0=np.array([F[0] for k in range(size)])
    dz=np.random.randn(size,nTimes[0])*np.sqrt(dates_lin[0][1])*sigma[0]
    
    res=timefunc(None, "numba (%d threads)" % nthreads, func_nb_mt, a, sigma, nTimes, dates_lin, r0, theta, dz)
    

    Que pourrais-je faire pour résoudre ce problème?

    (Le code ci-dessus est juste le bout de code avec numba et le multithreading, l'autre bout de fonction sans numba fonctionne parfaitement)

    -
    Edité par TiburceBaubeau 25 juillet 2019 à 18:25:48

    • Partager sur Facebook
    • Partager sur Twitter
      25 juillet 2019 à 21:56:18

      Pourquoi tu fais du multithreading ici ? c'est quoi l'objectif ? parce que ton code ne semble faire que des tâches orientées CPU. T'as un objectif de vitesse, cf. ta dernière ligne ? c'est ce qu'on se dit si on voit que  Numba est utilisé. Tu peux pas plutôt paralléliser mais je ne sais pas si ton algo s'y prête (c'est ce qu'on dirait vu le commentaire Spawn one thread per chunk)?

      -
      Edité par PascalOrtiz 25 juillet 2019 à 21:56:49

      • Partager sur Facebook
      • Partager sur Twitter
        26 juillet 2019 à 9:54:12

        Bonjour! Oui l'objectif c'est d'augmenter la vitesse de l'algorithme, j'ai vu que ça allait presque 4 fois plus vite quand j'utilisais 4 threads

        Par contre je dois stocker les variables dans des tableaux indépendants donc ça prend beaucoup de mémoire... c'est pour celà que je veux laisser la possibilité à l'utilisateur de choisir d'utiliser Numba ou non.

        Pour la parallélisation, j'ai déjà testé le décorateur @jit avec l'option parallel=True, mais en fait Numba est très peu performant pour détecter lui-même les parties du code qu'il pourrait paralleliser. Du coup je force la parallelisation avec la bibliothèque "threading"

        • Partager sur Facebook
        • Partager sur Twitter
          26 juillet 2019 à 11:24:58

          Juste une précision: tu ne vas rien accélérer avec threading (à cause de la GIL). Il faut utiliser multiprocessing et selon ton algo un dispositif de synchronisation comme Barrier ; et les performances seront sans doute moyennes (dans mes essais ce fut le cas).

          D'autre part, j'ai l'impression que tu as besoin de data parallelism donc pourquoi n'essaye tu pas les prange de Numba  ? Je ne suis pas du tout un spécialiste, je l'ai utilisé dans des cas simples, ça remplace range, parallélise et ça s'est montré plutôt efficcace, plus que des threads C++. Dans une autre direction t'as aussi du parallélisme avec Cython, géré en interne avec  openMP je crois.

          -
          Edité par PascalOrtiz 26 juillet 2019 à 11:34:27

          • Partager sur Facebook
          • Partager sur Twitter
            26 juillet 2019 à 13:52:30

            J'ai résolu le problème sans savoir comment, mais ça marche, j'ai tout recommencé de zéro en définissant mes variables une à une en dehors de la fonction principale.

            le GIL est délocké justement grâce à l'option (nogil=True) dans le décorateur numba, j'ai fait le test et je gagne un facteur 4 en terme de rapidité. (Par contre ça dépend vraiment du matériel utilisé, du nombre de coeurs dans le processeur etc...)

            Prange j'ai essayé mais çe ne semble pas fonctionner car le temps de calcul est le même voir plus élevé il me semble que quand je met seulement le décorateur @jit sans utiliser prange.

            Doit-on prendre des précautions particulières pour utiliser prange? Car dans tous les tests que j'ai fait les résultats montraient que prange ne marchait pas ou était moins efficace que d'utiliser seulement le décorateur @jit . (Pourtant j'ai fait plusieurs tests sur des fonctions vraiment toutes différentes les unes des autres)

            J'ai déjà entendu parler de Cython mais je ne l'ai jamais utilisé, je vais me renseigner si j'ai le temps... Comme je suis en stage et qu'il me reste seulement 1 mois je ne suis pas sûr que je pourrais regarder car il faut d'abord que je finisse tous les autres trucs que je dois faire.

            Merci pour ta réponse en tous cas, et si tu as des précisions  prange ça m’intéresse vraiment, je n'ai jamais réussi à utiliser ça correctement.

            -
            Edité par TiburceBaubeau 26 juillet 2019 à 13:55:16

            • Partager sur Facebook
            • Partager sur Twitter
              26 juillet 2019 à 14:13:17

              La fonction inner_func_nb contient deux boucles imbriquées écrites en Python. Clairement c'est le premier élément que j'essaierai d'éliminer en utilisant la vectorisation de Numpy. Un petit article parlant de la vectorisation : https://towardsdatascience.com/one-simple-trick-for-speeding-up-your-python-code-with-numpy-1afc846db418

              • Partager sur Facebook
              • Partager sur Twitter
                26 juillet 2019 à 14:32:50

                Dan737 a écrit:

                La fonction inner_func_nb contient deux boucles imbriquées écrites en Python. Clairement c'est le premier élément que j'essaierai d'éliminer en utilisant la vectorisation de Numpy. Un petit article parlant de la vectorisation : https://towardsdatascience.com/one-simple-trick-for-speeding-up-your-python-code-with-numpy-1afc846db418


                Je ne me suis pas embêté à faire ça car justement Numba va compiler tout sans passer par python, donc le fait de faire des boucles n'est plus un problème il me semble.

                (Je dis peut-être des conneries, mais je ne vois pas pourquoi Numpy qui est codé en C++ serait plus rapide que mon code qui va lui aussi être compilé en C++)

                • Partager sur Facebook
                • Partager sur Twitter
                  26 juillet 2019 à 14:48:42

                  Tu as raison, numba va compiler tout ça. Cependant il se peut que dans le code de numpy il y ait des portions de code qui relâche le GIL. De toute manière, perso je commencerais par ça, car il se peut que justement numba soit inutile. Avant de sortir l'artillerie lourde, j'essaie d'abord la solution la plus simple.
                  • Partager sur Facebook
                  • Partager sur Twitter
                    26 juillet 2019 à 15:21:40

                    Oui mais justement, j'ai fait en sorte de relâcher le GIL dans 100% de ma fonction principale "inner_func_nb"

                    Je vais regarder ton tuto sur la vectorisation, ça peut toujours me servir dans la partie sans numba... (L'avantage c'est qu'on a besoin de stocker beaucoup moins de variables en mémoire qu'avec la parallelisation)

                    • Partager sur Facebook
                    • Partager sur Twitter
                      26 juillet 2019 à 23:57:20

                      TiburceBaubeau a écrit:

                      le GIL est délocké justement grâce à l'option (nogil=True) dans le décorateur numba, 


                      Ah oui, bien vu, c'est vrai que Numba peut relâcher le verrou (comme Cython d'ailleurs), donc tu as eu une très bonne idée d'utiliser threading qui est beaucoup plus souple que multiprocessing, voilà qui ouvre des perpectives  intéressantes (voir aussi cet document).
                      Pour prange, je n'ai rien fait de sorcier. Je redonne le contexte de mon utilisation. C'était à propos de l'algorithme de Floyd-Warshall de tous les plus courts chemins (APSP en anglais) dans un graphe valué, ce qui en Python/Numba peut donner :
                      @jit(nopython=True)
                      def simple_np(M, n):
                          for k in range(n):
                              A=M[k]
                              for i in range(n):
                                  B=M[i]
                                  for j in range(n): 
                                      B[j]=min(B[k] + A[j],B[j])
                      
                      où M est une tableau Numpy de taille n x n. Comme on le voit c'est un triple boucle mais il est clair qu'on peut paralléliser les deux boucles internes, il faut simplement synchroniser à la fin de chaque étape de la boucle externe. Donc j'ai juste isolé une fonction pour que Numba puisse paralléliser, ce qui a donné 
                      @jit(nopython=True, parallel=True)
                      def mini_parallel(M, n, rowk, colk):
                          for i in prange(n):
                              for j in range(n):
                                  M[i][j]=min(rowk[j]+colk[i], M[i][j])
                      
                      @jit(nopython=True)
                      def parallel(M, n):
                          for k in range(n):
                              rowk=M[k]
                              colk=np.array([M[i][k] for i in range(n)])
                              mini_parallel(M, n, rowk, colk)
                      
                      Tu vois que j'ai demandé à Numba de paralléliser sur les lignes (mais ne pas emboîter deux prange comme j'ai fait dans mes premiers essais).
                      Résultat de tout ça sur un graphe de 800 sommets et 100000 arêtes. Le code Numba est à la base déjà très rapide (0.36s), il est plus rapide que 
                      - le Numpy vectorisé de la lib NetworkX (1.60s) 
                      - le C++/Boost utilisé par la lib graph-tools (0.90s)
                      - Scipy qui est en Cython (0.5s)
                      Le C++ est à peine plus rapide (0.30s).
                      Mais une fois parallélisé (4 coeurs), le temps passe à 0.11s, mieux que C++ avec des threads (0.14s) avec l'avantage de garder un vrai code Python très lisible et en changeant juste range en prange !
                      Voilà, je peux pas t'en dire tellement plus sur prange et d'ailleurs j'ai trouvé la doc de Numba un peu juste sur le coup (ils auraient pu donner l'exemple le plus bâteau qui soit du produit matriciel). Bon courage pour la suite de ton stage !

                      Dan737 a écrit:

                      Un meilleur article sur la vectorisation : https://realpython.com/numpy-array-programming/

                      J'aime bien aussi le document de Rougier

                      • Partager sur Facebook
                      • Partager sur Twitter

                      Problème avec Numba et le multithreading

                      × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                      × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                      • Editeur
                      • Markdown