@@ -54,7 +54,6 @@ import quantecon as qe
5454import matplotlib.pyplot as plt
5555```
565657-5857## Vue d'ensemble
59586059Dans un {doc}`cours précédent <need_for_speed>`, nous avons abordé la vectorisation,
@@ -140,7 +139,6 @@ with qe.Timer() as timer1:
140139141140```
142141143-144142#### Accélération via Numba
145143146144Pour accélérer la fonction `qm` à l'aide de Numba, nous importons d'abord la fonction `jit`
@@ -225,7 +223,6 @@ la syntaxe de *décorateur* et plaçons `@jit` avant la définition de la foncti
225223à ajouter `qm = jit(qm)` après la définition.
226224```
227225228-229226## Points délicats
230227231228Numba est relativement facile à utiliser mais pas toujours sans accroc.
@@ -275,7 +272,6 @@ iterate(g, 0.5, 100)
275272Dans d'autres cas, comme lorsque nous voulons utiliser des fonctions de bibliothèques externes
276273telles que `SciPy`, il pourrait ne pas y avoir de solution de contournement simple.
277274278-279275### Variables globales
280276281277Une autre chose à laquelle il faut faire attention lors de l'utilisation de Numba est la gestion des
@@ -474,7 +470,7 @@ Comparez la vitesse avec et sans Numba lorsque la taille de l'échantillon est g
474470:class: dropdown
475471```
476472477-Voici une solution :
473+Voici une solution :
478474479475```{code-cell} ipython3
480476@jit
@@ -491,7 +487,7 @@ def calculate_pi(u_draws, v_draws):
491487 return area_estimate * 4 # division par le rayon**2
492488```
493489494-Voyons maintenant à quelle vitesse cela s'exécute :
490+Voyons maintenant à quelle vitesse cela s'exécute :
495491496492```{code-cell} ipython3
497493with qe.Timer():
@@ -503,12 +499,12 @@ with qe.Timer():
503499 calculate_pi(u_draws, v_draws)
504500```
505501506-Si nous désactivons la compilation JIT en supprimant `@jit`, le code prend environ
507-150 fois plus de temps sur notre machine.
502+Si nous désactivons la compilation JIT en supprimant `@jit`, le code prend
503+considérablement plus de temps sur notre machine.
508504509-Nous obtenons donc un gain de vitesse de 2 ordres de grandeur en ajoutant quatre caractères.
505+Nous obtenons donc un gain de vitesse important en ajoutant quatre caractères.
510506511-La solution ci-dessus adopte l'une des deux approches naturelles : elle *tire tous les
507+La solution ci-dessus adopte l'une des deux approches naturelles : elle *tire tous les
512508points aléatoires d'abord*, les stocke dans `u_draws` et `v_draws`, puis laisse la
513509fonction jittée les parcourir en boucle.
514510@@ -533,6 +529,25 @@ with qe.Timer():
533529 calculate_pi_in_loop(rng, n)
534530```
535531532+```{code-cell} ipython3
533+with qe.Timer():
534+ calculate_pi_in_loop(rng, n)
535+```
536+537+Les deux cellules chronométrant la première approche ne mesurent que la boucle --- ses points
538+aléatoires sont tirés une seule fois dans le bloc de configuration partagé ci-dessus et ne sont jamais chronométrés, alors que
539+la seconde approche paie pour ses tirages à l'intérieur de la fonction chronométrée.
540+541+Pour comparer les deux approches de manière équitable, nous chronométrons la première approche de bout en bout,
542+en incluant le coût de la génération des tableaux :
543+544+```{code-cell} ipython3
545+with qe.Timer():
546+ u2 = rng.uniform(size=n)
547+ v2 = rng.uniform(size=n)
548+ calculate_pi(u2, v2)
549+```
550+536551Dans ce contexte séquentiel, les deux approches donnent des estimations tout aussi bonnes et s'exécutent à une
537552vitesse similaire, mais elles ne sont pas équivalentes en termes d'*utilisation de la mémoire*.
538553@@ -546,7 +561,7 @@ n'augmente pas avec `n`.
546561547562Cela pourrait suggérer que tirer à l'intérieur de la boucle est le meilleur choix par défaut.
548563549- Mais comme nous
564+Mais comme nous
550565le verrons dans {ref}`numba_ex_race`, tirer à l'intérieur de la boucle interagit
551566mal avec la parallélisation.
552567@@ -642,7 +657,7 @@ print(np.mean(x == 0)) # Fraction du temps où x est dans l'état 0
642657643658C'est (approximativement) la bonne sortie.
644659645-Chronométrons-le maintenant :
660+Chronométrons-le maintenant :
646661647662```{code-cell} ipython3
648663with qe.Timer():
@@ -669,7 +684,7 @@ with qe.Timer():
669684 compute_series_numba(n, U)
670685```
671686672-C'est une belle amélioration de vitesse pour une ligne de code !
687+C'est une belle amélioration de vitesse pour une ligne de code !
673688674689```{solution-end}
675690```
@@ -701,11 +716,11 @@ Pour la taille de la simulation Monte-Carlo, utilisez quelque chose de substanti
701716:class: dropdown
702717```
703718704-Voici une solution :
719+Voici une solution :
705720706721```{code-cell} ipython3
707722@jit(parallel=True)
708-def calculate_pi(u_draws, v_draws):
723+def calculate_pi_parallel(u_draws, v_draws):
709724 n = len(u_draws)
710725 count = 0
711726 for i in prange(n):
@@ -718,26 +733,26 @@ def calculate_pi(u_draws, v_draws):
718733 return area_estimate * 4 # division par le rayon**2
719734```
720735721-Voyons maintenant à quelle vitesse cela s'exécute :
736+Voyons maintenant à quelle vitesse cela s'exécute :
722737723738```{code-cell} ipython3
724739with qe.Timer():
725- calculate_pi(u_draws, v_draws)
740+ calculate_pi_parallel(u_draws, v_draws)
726741```
727742728743```{code-cell} ipython3
729744with qe.Timer():
730- calculate_pi(u_draws, v_draws)
745+ calculate_pi_parallel(u_draws, v_draws)
731746```
732747733748En activant et désactivant la parallélisation (en choisissant `True` ou
734749`False` dans l'annotation `@jit`), nous pouvons tester le gain de vitesse que
735750le multithreading apporte en plus de la compilation JIT.
736751737-Sur notre station de travail, nous constatons que la parallélisation augmente la vitesse d'exécution d'un
738-facteur de 2 ou 3.
752+Sur notre station de travail, nous constatons que la parallélisation apporte ici un gain de
753+vitesse modeste mais appréciable.
739754740-(Si vous exécutez localement, vous obtiendrez des nombres différents, dépendant principalement
755+(Si vous exécutez localement, vous obtiendrez des résultats différents, dépendant principalement
741756du nombre de CPU sur votre machine.)
742757743758Remarquez que nous avons tiré tous les points aléatoires *avant* la boucle et les avons passés
@@ -760,16 +775,16 @@ Dans {ref}`numba_ex3`, nous avons tiré tous les points aléatoires *avant* la b
760775761776Il est tentant de plutôt tirer chaque point *à l'intérieur* de la boucle `prange`, en passant un générateur `rng` en argument et en appelant `rng.uniform()` dans le corps de la boucle.
762777763-Essayez-le : le code devrait s'exécuter et renvoyer un nombre proche de $\pi$, pourtant il y a un bug subtil dans cette approche.
778+Essayez-le : le code devrait s'exécuter et renvoyer un nombre proche de $\pi$, pourtant il y a un bug subtil dans cette approche.
764779765-Enquêtez comme suit :
780+Enquêtez comme suit :
7667817677821. Appelez votre fonction quelques fois avec la *même* graine et vérifiez si le résultat est reproductible.
7687832. Répétez l'estimation de nombreuses fois sur une gamme de tailles d'échantillon et comparez sa dispersion à celle d'une version parallèle correcte.
769784770785Expliquez ensuite ce qui ne va pas et donnez une manière correcte de tirer à l'intérieur d'une boucle parallèle.
771786772-Astuce : essayez d'utiliser une fonction aléatoire ancienne telle que `np.random.uniform()` au lieu d'un `Generator` et voyez ce qui se passe.
787+Astuce : essayez d'utiliser une fonction aléatoire ancienne telle que `np.random.uniform()` au lieu d'un `Generator` et voyez ce qui se passe.
773788```
774789775790```{solution-start} numba_ex_race
@@ -785,7 +800,7 @@ n = 1_000_000
785800rng = np.random.default_rng()
786801787802@jit(parallel=True)
788-def calculate_pi_in_loop(rng, n):
803+def calculate_pi_in_loop_parallel(rng, n):
789804 count = 0
790805 for i in prange(n):
791806 u, v = rng.uniform(), rng.uniform()
@@ -794,7 +809,7 @@ def calculate_pi_in_loop(rng, n):
794809 count += 1
795810 return (count / n) * 4
796811797-calculate_pi_in_loop(rng, n)
812+calculate_pi_in_loop_parallel(rng, n)
798813```
799814800815Le code s'exécute sans erreur et renvoie quelque chose de proche de $\pi$.
@@ -814,20 +829,20 @@ imprévisible.
814829815830Deux symptômes révèlent le problème.
816831817-*Symptôme 1 : le résultat n'est plus reproductible.*
832+*Symptôme 1 : le résultat n'est plus reproductible.*
818833819834Un générateur correct renvoie la même réponse chaque fois qu'on lui donne la même graine.
820835821836À cause de la course aux données, l'ordre dans lequel les threads touchent l'état partagé affecte le flux de tirages, de sorte que la réponse n'est pas reproductible même lorsque la graine est fixée.
822837823838```{code-cell} ipython3
824839for seed in (1, 1, 1):
825- print(calculate_pi_in_loop(np.random.default_rng(seed), n))
840+ print(calculate_pi_in_loop_parallel(np.random.default_rng(seed), n))
826841```
827842828843Chaque appel utilise la même graine, pourtant les réponses diffèrent.
829844830-*Symptôme 2 : l'estimateur est bien plus bruité qu'il ne devrait l'être.*
845+*Symptôme 2 : l'estimateur est bien plus bruité qu'il ne devrait l'être.*
831846832847Les tirages dupliqués et corrélés portent moins d'information que $n$ tirages indépendants, de sorte que la taille d'échantillon *effective* est bien plus petite que $n$.
833848@@ -854,7 +869,7 @@ num_reps = 20
854869methods = [("état par thread (correct)",
855870 lambda n: calculate_pi_legacy(n), 'C0'),
856871 ("générateur partagé dans prange (course aux données)",
857- lambda n: calculate_pi_in_loop(np.random.default_rng(), n), 'C1')]
872+ lambda n: calculate_pi_in_loop_parallel(np.random.default_rng(), n), 'C1')]
858873859874fig, ax = plt.subplots()
860875for label, estimate, color in methods:
@@ -874,7 +889,7 @@ plt.show()
874889875890Les deux bandes sont centrées sur $\pi$, mais la bande associée à la course aux données est bien plus large que l'autre et se rétrécit très lentement à mesure que la taille de l'échantillon augmente.
876891877-L'autre option sûre est celle de {ref}`numba_ex3` : tirer les points avant la boucle afin que la boucle parallèle ne fasse que lire depuis la mémoire.
892+L'autre option sûre est celle de {ref}`numba_ex3` : tirer les points avant la boucle afin que la boucle parallèle ne fasse que lire depuis la mémoire.
878893879894```{solution-end}
880895```
@@ -905,7 +920,7 @@ rng = np.random.default_rng()
905920with qe.Timer():
906921 u_draws = rng.uniform(size=n)
907922 v_draws = rng.uniform(size=n)
908- calculate_pi(u_draws, v_draws)
923+ calculate_pi_parallel(u_draws, v_draws)
909924```
910925911926```{code-cell} ipython3
@@ -1000,8 +1015,17 @@ $$
1000101510011016En utilisant ce fait, la solution peut s'écrire comme suit.
100210171003-Notez que les tirages aléatoires sont conservés à l'intérieur de la boucle interne plutôt que pré-alloués,
1004-afin d'éviter de créer de grands tableaux de chocs de taille `M * n`.
1018+```{note}
1019+Ici, nous conservons les tirages aléatoires à l'intérieur de la boucle interne et utilisons l'ancienne
1020+API `np.random.randn()` plutôt qu'un `Generator`.
1021+1022+En effet, le support de Numba pour les objets `Generator` n'est pas
1023+[thread-safe](https://numba.readthedocs.io/en/stable/reference/numpysupported.html#generator-objects)
1024+sous exécution parallèle (`@jit(parallel=True)`).
1025+1026+Pré-tirer les chocs dans des tableaux de forme `(M, n)` éviterait ce problème mais est
1027+peu pratique ici, car `M = 10_000_000` nécessiterait plusieurs Go de mémoire.
1028+```
100510291006103010071031```{code-cell} ipython3
@@ -1040,4 +1064,4 @@ Essayez de basculer entre `parallel=True` et `parallel=False` et notez le temps
10401064Si vous êtes sur une machine avec de nombreux CPU, la différence devrait être significative.
1041106510421066```{solution-end}
1043-```
1067+```