Skip to content

Commit 84f95d1

Browse files
committed
Update translation: lectures/numba.md
1 parent 819f221 commit 84f95d1

1 file changed

Lines changed: 59 additions & 35 deletions

File tree

lectures/numba.md

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ import quantecon as qe
5454
import matplotlib.pyplot as plt
5555
```
5656

57-
5857
## Vue d'ensemble
5958

6059
Dans un {doc}`cours précédent <need_for_speed>`, nous avons abordé la vectorisation,
@@ -140,7 +139,6 @@ with qe.Timer() as timer1:
140139
141140
```
142141

143-
144142
#### Accélération via Numba
145143

146144
Pour 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
```
227225

228-
229226
## Points délicats
230227

231228
Numba est relativement facile à utiliser mais pas toujours sans accroc.
@@ -275,7 +272,6 @@ iterate(g, 0.5, 100)
275272
Dans d'autres cas, comme lorsque nous voulons utiliser des fonctions de bibliothèques externes
276273
telles que `SciPy`, il pourrait ne pas y avoir de solution de contournement simple.
277274

278-
279275
### Variables globales
280276

281277
Une 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
```
476472

477-
Voici une solution :
473+
Voici une solution :
478474

479475
```{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
```
493489

494-
Voyons maintenant à quelle vitesse cela s'exécute :
490+
Voyons maintenant à quelle vitesse cela s'exécute :
495491

496492
```{code-cell} ipython3
497493
with qe.Timer():
@@ -503,12 +499,12 @@ with qe.Timer():
503499
calculate_pi(u_draws, v_draws)
504500
```
505501

506-
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.
508504

509-
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.
510506

511-
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
512508
points aléatoires d'abord*, les stocke dans `u_draws` et `v_draws`, puis laisse la
513509
fonction jittée les parcourir en boucle.
514510

@@ -533,6 +529,25 @@ with qe.Timer():
533529
calculate_pi_in_loop(rng, n)
534530
```
535531

532+
```{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+
536551
Dans ce contexte séquentiel, les deux approches donnent des estimations tout aussi bonnes et s'exécutent à une
537552
vitesse 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`.
546561

547562
Cela pourrait suggérer que tirer à l'intérieur de la boucle est le meilleur choix par défaut.
548563

549-
Mais comme nous
564+
Mais comme nous
550565
le verrons dans {ref}`numba_ex_race`, tirer à l'intérieur de la boucle interagit
551566
mal avec la parallélisation.
552567

@@ -642,7 +657,7 @@ print(np.mean(x == 0)) # Fraction du temps où x est dans l'état 0
642657

643658
C'est (approximativement) la bonne sortie.
644659

645-
Chronométrons-le maintenant :
660+
Chronométrons-le maintenant :
646661

647662
```{code-cell} ipython3
648663
with qe.Timer():
@@ -669,7 +684,7 @@ with qe.Timer():
669684
compute_series_numba(n, U)
670685
```
671686

672-
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 !
673688

674689
```{solution-end}
675690
```
@@ -701,11 +716,11 @@ Pour la taille de la simulation Monte-Carlo, utilisez quelque chose de substanti
701716
:class: dropdown
702717
```
703718

704-
Voici une solution :
719+
Voici une solution :
705720

706721
```{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
```
720735

721-
Voyons maintenant à quelle vitesse cela s'exécute :
736+
Voyons maintenant à quelle vitesse cela s'exécute :
722737

723738
```{code-cell} ipython3
724739
with qe.Timer():
725-
calculate_pi(u_draws, v_draws)
740+
calculate_pi_parallel(u_draws, v_draws)
726741
```
727742

728743
```{code-cell} ipython3
729744
with qe.Timer():
730-
calculate_pi(u_draws, v_draws)
745+
calculate_pi_parallel(u_draws, v_draws)
731746
```
732747

733748
En 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
735750
le multithreading apporte en plus de la compilation JIT.
736751

737-
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.
739754

740-
(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
741756
du nombre de CPU sur votre machine.)
742757

743758
Remarquez 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
760775
761776
Il 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.
762777
763-
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.
764779
765-
Enquêtez comme suit :
780+
Enquêtez comme suit :
766781
767782
1. Appelez votre fonction quelques fois avec la *même* graine et vérifiez si le résultat est reproductible.
768783
2. 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.
769784
770785
Expliquez ensuite ce qui ne va pas et donnez une manière correcte de tirer à l'intérieur d'une boucle parallèle.
771786
772-
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
```
774789

775790
```{solution-start} numba_ex_race
@@ -785,7 +800,7 @@ n = 1_000_000
785800
rng = np.random.default_rng()
786801
787802
@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
796811
797-
calculate_pi_in_loop(rng, n)
812+
calculate_pi_in_loop_parallel(rng, n)
798813
```
799814

800815
Le code s'exécute sans erreur et renvoie quelque chose de proche de $\pi$.
@@ -814,20 +829,20 @@ imprévisible.
814829

815830
Deux symptômes révèlent le problème.
816831

817-
*Symptôme 1 : le résultat n'est plus reproductible.*
832+
*Symptôme 1 : le résultat n'est plus reproductible.*
818833

819834
Un générateur correct renvoie la même réponse chaque fois qu'on lui donne la même graine.
820835

821836
À 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.
822837

823838
```{code-cell} ipython3
824839
for 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
```
827842

828843
Chaque appel utilise la même graine, pourtant les réponses diffèrent.
829844

830-
*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.*
831846

832847
Les 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
854869
methods = [("é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')]
858873
859874
fig, ax = plt.subplots()
860875
for label, estimate, color in methods:
@@ -874,7 +889,7 @@ plt.show()
874889

875890
Les 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.
876891

877-
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.
878893

879894
```{solution-end}
880895
```
@@ -905,7 +920,7 @@ rng = np.random.default_rng()
905920
with 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
```
910925

911926
```{code-cell} ipython3
@@ -1000,8 +1015,17 @@ $$
10001015

10011016
En utilisant ce fait, la solution peut s'écrire comme suit.
10021017

1003-
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+
```
10051029

10061030

10071031
```{code-cell} ipython3
@@ -1040,4 +1064,4 @@ Essayez de basculer entre `parallel=True` et `parallel=False` et notez le temps
10401064
Si vous êtes sur une machine avec de nombreux CPU, la différence devrait être significative.
10411065

10421066
```{solution-end}
1043-
```
1067+
```

0 commit comments

Comments
 (0)