9. Python pour le calcul scientifique#

« Nous devrions oublier les petites optimisations, disons environ 97 % du temps : l’optimisation prématurée est la source de tous les maux. » – Donald Knuth

9.1. Vue d’ensemble#

Python est le langage le plus populaire pour de nombreux aspects du calcul scientifique.

Cela s’explique par

  • la nature accessible et expressive du langage lui-même,

  • l’immense gamme de bibliothèques scientifiques de haute qualité,

  • le fait que le langage et les bibliothèques soient open source,

  • le rôle central que joue Python dans la science des données, l’apprentissage automatique et l’IA.

Dans les cours précédents, nous avons utilisé quelques bibliothèques scientifiques Python, notamment NumPy et Matplotlib.

Cependant, notre attention principale portait sur le langage Python de base, plutôt que sur les bibliothèques.

Nous nous tournons maintenant vers les bibliothèques scientifiques et leur accordons toute notre attention.

Dans ce cours introductif, nous aborderons les sujets suivants :

  1. Quels sont les principaux éléments de l’écosystème scientifique Python ?

  2. Comment s’articulent-ils entre eux ?

  3. Comment la situation évolue-t-elle au fil du temps ?

En plus de ce qui est inclus dans Anaconda, ce cours nécessitera

!pip install quantecon

Hide code cell output

Requirement already satisfied: quantecon in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.11.4)
Requirement already satisfied: numba>=0.49.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (0.65.1)
Requirement already satisfied: numpy>=1.17.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.4.6)
Requirement already satisfied: requests in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.34.2)
Requirement already satisfied: scipy>=1.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.17.1)
Requirement already satisfied: sympy in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.14.0)
Requirement already satisfied: llvmlite<0.48,>=0.47.0dev0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numba>=0.49.0->quantecon) (0.47.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2026.5.20)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sympy->quantecon) (1.3.0)

Commençons par quelques importations :

import numpy as np
import quantecon as qe
import matplotlib.pyplot as plt
import random

9.2. Principales bibliothèques scientifiques#

Passons brièvement en revue les bibliothèques scientifiques de Python.

9.2.1. Pourquoi en avons-nous besoin ?#

Nous avons besoin des bibliothèques scientifiques de Python pour deux raisons :

  1. Python est petit

  2. Python est lent

Python est petit

Le cœur de Python est petit par conception – cela facilite l’optimisation, l’accessibilité et la maintenance

Les bibliothèques scientifiques fournissent les routines que nous ne voulons pas – et ne devrions probablement pas – écrire nous-mêmes

  • intégration numérique, interpolation, algèbre linéaire, recherche de racines, etc.

Python est lent

Une autre raison pour laquelle nous avons besoin des bibliothèques scientifiques est que le Python pur est relativement lent.

Les bibliothèques scientifiques accélèrent l’exécution en utilisant trois stratégies principales :

  1. Vectorisation : fourniture de code machine compilé et d’interfaces qui rendent ce code accessible

  2. Compilation JIT : compilateurs qui convertissent des instructions de type Python en code machine rapide au moment de l’exécution

  3. Parallélisation : répartition des tâches sur plusieurs threads / CPU / GPU / TPU

Nous discuterons de ces idées en profondeur ci-dessous.

9.2.2. L’écosystème scientifique de Python#

À QuantEcon, les bibliothèques scientifiques que nous utilisons le plus souvent sont

Voici comment elles s’articulent :

  • NumPy pose les fondations en fournissant un type de données tableau de base (pensez aux vecteurs et aux matrices) et des fonctions pour agir sur ces tableaux (par exemple, la multiplication matricielle).

  • SciPy s’appuie sur NumPy en ajoutant des méthodes numériques couramment utilisées en science (interpolation, optimisation, recherche de racines, etc.).

  • Matplotlib est utilisé pour générer des figures, en mettant l’accent sur le tracé de données stockées dans des tableaux NumPy.

  • JAX inclut des opérations de traitement de tableaux similaires à NumPy, la différentiation automatique, un compilateur juste-à-temps centré sur la parallélisation, et une intégration automatisée avec des accélérateurs matériels tels que les GPU.

  • Pandas fournit des types et des fonctions pour manipuler les données.

  • Numba fournit un compilateur juste-à-temps qui fonctionne bien avec NumPy et aide à accélérer le code Python.

Nous discuterons de toutes ces bibliothèques en détail dans cette série de cours.

9.3. Pourquoi le Python pur est-il lent ?#

Comme mentionné ci-dessus, le code numérique écrit en Python pur est relativement lent.

Essayons de comprendre ce qui explique les vitesses d’exécution lentes.

9.3.1. Vérification de type#

Une source de surcoût dans les opérations en Python pur est la vérification de type.

Essayons de comprendre les enjeux.

9.3.1.1. Typage dynamique#

Considérons cette opération Python

a, b = 10, 10
a + b
20

Même pour cette opération simple, l’interpréteur Python a pas mal de travail à faire.

Par exemple, dans l’instruction a + b, l’interpréteur doit savoir quelle opération invoquer.

Si a et b sont des chaînes de caractères, alors a + b nécessite une concaténation de chaînes

a, b = 'foo', 'bar'
a + b
'foobar'

Si a et b sont des listes, alors a + b nécessite une concaténation de listes

a, b = ['foo'], ['bar']
a + b
['foo', 'bar']

En conséquence, lors de l’exécution de a + b, Python doit d’abord vérifier le type des objets, puis appeler l’opération correcte.

Cela implique un surcoût.

Si nous exécutons cette expression de manière répétée dans une boucle serrée, le surcoût devient important.

9.3.1.2. Types statiques#

Les langages compilés évitent ces surcoûts avec des types explicites et statiques.

Par exemple, considérons le code C suivant, qui additionne les entiers de 1 à 10

#include <stdio.h>

int main(void) {
    int i;
    int sum = 0;
    for (i = 1; i <= 10; i++) {
        sum = sum + i;
    }
    printf("sum = %d\n", sum);
    return 0;
}

Les variables i et sum sont explicitement déclarées comme des entiers.

De plus, lorsque nous faisons une instruction telle que int i, nous faisons une promesse au compilateur que i sera toujours un entier, tout au long de l’exécution du programme.

De ce fait, la signification de l’addition dans l’expression sum + i est totalement sans ambiguïté.

Il n’y a pas besoin de vérification de type et donc pas de surcoût.

9.3.2. Accès aux données#

Un autre frein à la vitesse pour les langages de haut niveau est l’accès aux données.

Pour illustrer, considérons le problème de la sommation de données — disons, une collection d’entiers.

9.3.2.1. Sommation avec du code compilé#

En C ou en Fortran, un tableau d’entiers est stocké dans un seul bloc contigu de mémoire

  • Par exemple, un entier de 64 bits est stocké dans 8 octets de mémoire.

  • Un tableau de \(n\) tels entiers occupe \(8n\) octets consécutifs.

De plus, le type de données est connu au moment de la compilation.

Ainsi, chaque point de données successif peut être accédé en avançant dans l’espace mémoire d’une quantité connue et fixe.

9.3.2.2. Sommation en Python pur#

Python essaie de reproduire ces idées dans une certaine mesure.

Par exemple, dans l’implémentation standard de Python (CPython), les éléments d’une liste sont placés dans des emplacements mémoire qui sont dans un certain sens contigus.

Cependant, ces éléments de liste ressemblent davantage à des pointeurs vers des données qu’à des données réelles.

Ainsi, il y a toujours un surcoût impliqué dans l’accès aux valeurs de données elles-mêmes.

Un tel surcoût est un coupable majeur en ce qui concerne l’exécution lente.

9.3.3. Résumé#

La discussion ci-dessus signifie-t-elle que nous devrions simplement passer à C ou Fortran pour tout ?

La réponse est : Certainement pas !

Pour tout programme donné, relativement peu de lignes seront jamais critiques en termes de temps.

Il est donc bien plus efficace d’écrire la majeure partie de notre code dans un langage à haute productivité comme Python.

De plus, même pour les lignes de code qui sont critiques en termes de temps, nous pouvons désormais égaler ou surpasser les binaires compilés à partir de C ou Fortran en utilisant les bibliothèques scientifiques de Python.

À ce propos, nous soulignons que, ces dernières années, accélérer le code est devenu essentiellement synonyme de parallélisation.

Cette tâche est mieux laissée aux compilateurs spécialisés !

9.4. Accélérer Python#

Dans cette section, nous examinons trois techniques connexes pour accélérer le code Python.

Ici, nous nous concentrerons sur les idées fondamentales.

Plus tard, nous examinerons des bibliothèques spécifiques et comment elles implémentent ces idées.

9.4.1. Vectorisation#

Une méthode pour éviter le trafic mémoire et la vérification de type est la programmation par tableaux.

De nombreux économistes désignent généralement la programmation par tableaux par le terme « vectorisation ».

Note

En informatique, ce terme a une signification légèrement différente.

L’idée clé est d’envoyer les opérations de traitement de tableaux par lots à du code machine natif précompilé et efficace.

Le code machine lui-même est généralement compilé à partir de C ou de Fortran soigneusement optimisé.

Par exemple, lorsqu’on travaille dans un langage de haut niveau, l’opération d’inversion d’une grande matrice peut être sous-traitée à du code machine efficace qui est précompilé à cette fin et fourni aux utilisateurs dans le cadre d’un package.

Les principaux avantages sont

  1. la vérification de type est payée par tableau, plutôt que par élément, et

  2. les tableaux contenant des éléments du même type de données sont efficaces en termes d’accès mémoire.

L’idée de la vectorisation remonte à MATLAB, qui utilise la vectorisation de manière intensive.

_images/matlab.png

NumPy utilise un modèle similaire, inspiré de MATLAB

9.4.2. Vectorisation vs boucles Python pures#

Essayons une comparaison rapide de vitesse pour illustrer comment la vectorisation peut accélérer le code.

Voici du code non vectorisé, qui utilise une boucle Python native pour générer, mettre au carré, puis additionner un grand nombre de variables aléatoires :

n = 1_000_000
with qe.Timer():
    y = 0      # Accumulera et stockera la somme
    for i in range(n):
        x = random.uniform(0, 1)
        y += x**2
0.2508 seconds elapsed

Le code vectorisé suivant utilise NumPy, que nous étudierons bientôt en détail, pour parvenir au même résultat.

rng = np.random.default_rng()
with qe.Timer():
    x = rng.uniform(0, 1, n)
    y = np.sum(x**2)
0.0075 seconds elapsed

Comme vous pouvez le voir, le deuxième bloc de code s’exécute beaucoup plus rapidement.

Il décompose la boucle en trois opérations de base

  1. tirer n uniformes

  2. les mettre au carré

  3. les additionner

Ces opérations sont envoyées comme opérateurs par lots à du code machine optimisé.

9.4.3. Compilateurs JIT#

Au mieux, la vectorisation produit du code rapide et simple.

Cependant, elle n’est pas sans inconvénients.

Un problème est qu’elle peut être très gourmande en mémoire.

Cela est dû au fait que la vectorisation tend à créer de nombreux tableaux intermédiaires avant de produire le calcul final.

Un autre problème est que tous les algorithmes ne peuvent pas être vectorisés.

En raison de ces problèmes, la plupart du calcul haute performance s’éloigne de la vectorisation traditionnelle et se tourne vers l’utilisation de compilateurs juste-à-temps.

Dans les cours ultérieurs de cette série, nous apprendrons comment les bibliothèques Python modernes exploitent les compilateurs juste-à-temps pour générer du code machine rapide, efficace et parallélisé.

9.5. Parallélisation#

La croissance de la fréquence d’horloge des CPU (c’est-à-dire la vitesse à laquelle une seule chaîne logique peut être exécutée) a considérablement ralenti ces dernières années.

Les concepteurs de puces et les programmeurs ont répondu à ce ralentissement en cherchant une voie différente vers une exécution rapide : la parallélisation.

Cela implique

  1. d’augmenter le nombre de CPU intégrés dans chaque machine

  2. de connecter des accélérateurs matériels tels que les GPU et les TPU

Pour les programmeurs, le défi a été d’exploiter ce matériel en exécutant de nombreux processus en parallèle.

Ci-dessous, nous discutons de la parallélisation pour le calcul scientifique, en mettant l’accent sur

  1. les outils de parallélisation en Python et

  2. comment ces outils peuvent être appliqués à des problèmes économiques quantitatifs.

9.5.1. Parallélisation sur les CPU#

Passons en revue les deux principaux types de parallélisation basée sur les CPU couramment utilisés dans le calcul scientifique et discutons de leurs avantages et inconvénients.

9.5.1.1. Multithreading#

Le multithreading signifie exécuter plusieurs threads d’exécution au sein d’un seul processus.

Tous les threads partagent le même espace mémoire, ils peuvent donc lire et écrire dans les mêmes tableaux sans copier de données.

Par exemple, lorsqu’une opération numérique sur un grand tableau s’exécute sur un ordinateur portable moderne, la charge de travail peut être répartie sur les multiples cœurs de CPU de la machine, chaque cœur traitant une portion du tableau.

Note

Le Python natif peine à implémenter le multithreading en raison de certaines caractéristiques de conception héritées. Mais ce n’est pas une restriction pour les bibliothèques scientifiques comme NumPy et Numba. Les fonctions importées de ces bibliothèques et le code compilé en JIT s’exécutent dans des environnements d’exécution de bas niveau où les restrictions héritées de Python ne s’appliquent pas.

9.5.1.2. Multiprocessing#

Le multiprocessing signifie exécuter plusieurs processus indépendants, chacun avec son propre espace mémoire séparé.

Comme la mémoire n’est pas partagée, les processus communiquent en s’échangeant des données.

Le multiprocessing peut s’exécuter sur une seule machine ou être distribué sur un cluster de machines connectées par un réseau.

9.5.1.3. Que devrions-nous utiliser ?#

Pour le travail numérique sur une seule machine, le multithreading est généralement préféré — il est léger et le modèle de mémoire partagée est très pratique.

Le multiprocessing devient important lors du passage à l’échelle au-delà d’une seule machine.

Pour la grande majorité de ce que nous faisons dans ces cours, le multithreading suffira.

9.5.2. Accélérateurs matériels#

Une source de parallélisme plus spectaculaire provient d’accélérateurs matériels spécialisés, en particulier les GPU (Graphics Processing Units, unités de traitement graphique).

Les GPU ont été conçus à l’origine pour le rendu graphique, qui nécessite d’effectuer la même opération sur de nombreux pixels simultanément.

_images/geforce.png

Cette architecture — des milliers de cœurs simples exécutant la même instruction sur différents points de données — s’avère idéale pour le calcul scientifique.

Note

Un cœur est une unité de traitement indépendante au sein d’une puce — un circuit qui peut exécuter des instructions par lui-même. Un CPU possède généralement un petit nombre de cœurs puissants, chacun capable de gérer des séquences complexes d’opérations. Un GPU regroupe plutôt des milliers de cœurs plus petits et plus simples, chacun conçu pour effectuer des opérations arithmétiques de base. La puissance du GPU vient du fait que tous ces cœurs travaillent simultanément sur différentes parties du même problème.

Lorsqu’un calcul peut être exprimé comme des opérations indépendantes sur de grands tableaux de données, les GPU peuvent être plusieurs ordres de grandeur plus rapides que les CPU.

Les TPU (Tensor Processing Units, unités de traitement tensoriel), conçus par Google pour l’apprentissage automatique, suivent une philosophie similaire, en optimisant pour des opérations matricielles parallèles massives.

9.5.3. Accéder aux ressources GPU#

De nombreuses stations de travail et ordinateurs portables sont désormais équipés de GPU performants, et un seul GPU moderne est souvent suffisant pour des projets de recherche individuels.

Les bibliothèques Python modernes comme JAX, largement abordée dans cette série de cours, détectent et utilisent automatiquement les GPU disponibles avec un minimum de modifications de code.

Pour les problèmes à plus grande échelle, les serveurs multi-GPU (souvent 4 à 8 GPU par machine) sont de plus en plus courants.

_images/dgx.png

Avec un logiciel approprié, les calculs peuvent être distribués sur plusieurs GPU, que ce soit au sein d’un seul serveur ou sur un cluster.

Nous explorerons le calcul GPU plus en détail dans les cours ultérieurs, en l’appliquant à une gamme d’applications économiques.