GitHub

@@ -54,7 +54,6 @@ import quantecon as qe

5454

import matplotlib.pyplot as plt

5555

```

565657-5857

## Vue d'ensemble

59586059

Dans 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

145143146144

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

```

227225228-229226

## Points délicats

230227231228

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.

277274278-279275

### Variables globales

280276281277

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

```

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

497493

with 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

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

```

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+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`.

546561547562

Cela 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

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

642657643658

C'est (approximativement) la bonne sortie.

644659645-

Chronométrons-le maintenant :

660+

Chronométrons-le maintenant :

646661647662

```{code-cell} ipython3

648663

with 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

724739

with qe.Timer():

725-

calculate_pi(u_draws, v_draws)

740+

calculate_pi_parallel(u_draws, v_draws)

726741

```

727742728743

```{code-cell} ipython3

729744

with qe.Timer():

730-

calculate_pi(u_draws, v_draws)

745+

calculate_pi_parallel(u_draws, v_draws)

731746

```

732747733748

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.

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

741756

du nombre de CPU sur votre machine.)

742757743758

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

760775761776

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.

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 :

766781767782

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.

769784770785

Expliquez 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

785800

rng = 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

```

799814800815

Le code s'exécute sans erreur et renvoie quelque chose de proche de $\pi$.

@@ -814,20 +829,20 @@ imprévisible.

814829815830

Deux 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.*

818833819834

Un 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

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

```

827842828843

Chaque 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.*

831846832847

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')]

858873859874

fig, ax = plt.subplots()

860875

for label, estimate, color in methods:

@@ -874,7 +889,7 @@ plt.show()

874889875890

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.

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

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

```

910925911926

```{code-cell} ipython3

@@ -1000,8 +1015,17 @@ $$

1000101510011016

En 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

10401064

Si vous êtes sur une machine avec de nombreux CPU, la différence devrait être significative.

1041106510421066

```{solution-end}

1043-

```

1067+

```

Read the original on github.com ↗