Chad Schuster, Principal au sein de la pratique de gestion des risques financiers de Milliman, a présenté une expérience pratique visant à construire des modèles computationnels haute performance avec Python et Numba, plutôt que de s’appuyer entièrement sur des applications écrites en C++. L’expérience part d’un problème courant dans les services financiers : les modèles qui simulent les flux de trésorerie sur un grand nombre de scénarios nécessitent une importante capacité de calcul, tandis que les équipes souhaitent conserver la rapidité de développement et la facilité de maintenance de Python.
Le problème revêt une importance particulière dans les modèles d’assurance vie et les produits de retraite tels que les annuities, où les calculs servent à évaluer les engagements futurs et à exécuter de multiples scénarios. Selon la présentation, les compagnies d’assurance se sont historiquement appuyées sur des grilles locales comprenant entre 5 000 et 10 000 nœuds, mais le passage au cloud a rendu le temps d’exécution et le coût de chaque opération plus visibles dans les décisions d’infrastructure.
Qu’apporte Numba à Python ?
Python traditionnelle, ou CPython, interprète le code pendant l’exécution, tandis que les langages compilés comme C++ transforment le code en instructions machine avant l’exécution. Numba tente de réduire cet écart grâce à la compilation juste-à-temps (Just-in-Time), en compilant les fonctions éligibles pendant l’exécution du programme à l’aide de LLVM, puis en remplaçant la fonction originale par l’implémentation compilée.
Le développeur active généralement ce processus au moyen de décorateurs Python tels que JIT ou njit. Numba fonctionne au niveau de la fonction : elle examine le code et le transforme en une représentation intermédiaire qui lui est propre, puis infère les types des variables et des paramètres ainsi que les valeurs de retour, avant d’abaisser cette représentation en LLVM IR et d’effectuer les optimisations nécessaires. Si la fonction est appelée avec des types différents, Numba peut créer des implémentations compilées spécialisées pour chaque ensemble de types grâce à ce qu’elle appelle le polymorphic dispatch.
Les gains de performance ne constituent pas un chiffre fixe
Dans la preuve de concept présentée par Schuster, Numba a rendu le programme environ 75 fois plus rapide que Python interprété. Dans un autre modèle, le résultat était différent : le transfert des calculs numériques intensifs vers Numba sur le processeur central a permis une accélération d’environ deux fois, puis le passage au GPU a apporté une accélération supplémentaire de 750 fois lors de cette exécution précise. La présentation indiquait que ce niveau d’amélioration rendait l’unité GPU à peu près équivalente à 750 cœurs utilisés lors de cette exécution, avec une réduction estimée du coût à environ un dixième.
Ces résultats ne constituent toutefois pas une promesse générale pour toutes les applications. L’amélioration dépend de la quantité de code compilable, du volume des opérations de calcul par rapport aux opérations d’entrée-sortie, de l’efficacité du Python original et de la capacité de LLVM à optimiser le code produit. En outre, le traitement sur GPU ne convient pas à tous les algorithmes ; il faut donc mesurer le véritable goulot d’étranglement avant de reconcevoir le modèle.
Qu’est-ce qui change concrètement pour les équipes d’ingénierie ?
L’expérience recommande de conserver dans Python les couches de préparation des données ainsi que les entrées et sorties, et de ne transférer vers Numba que la partie numérique intensive chaque fois que possible. Cette approche limite l’ampleur de la réécriture et préserve une grande partie de l’environnement Python, tout en concentrant les efforts d’optimisation sur les fonctions qui consomment la majeure partie du temps d’exécution. Lorsque les fonctions numériques sont limitées et clairement définies, Schuster estime que l’expérimentation avec Numba peut constituer une option directe pour ceux qui souhaitent rester dans Python.
En revanche, l’introduction d’une logique étendue et complexe dans Numba accroît les coûts de conception et de maintenance. L’expérience s’est donc appuyée sur des tableaux NumPy, des tuples et des structures de données simples, car la prise en charge de Numba ne couvre pas toutes les fonctionnalités de Python. Les limites citées comprennent les dictionnaires aux types flexibles, les exceptions, les gestionnaires de contexte, les fermetures et les listes créées au moyen de comprehensions, ainsi que la prise en charge limitée de certaines fonctions courantes telles que print, sorted et getattr.
Les limites à prendre en compte avant toute adoption
La programmation orientée objet demeure un point faible important dans cette expérience. Numba fournit des fonctionnalités expérimentales telles que jitclasses et structrefs pour apporter un comportement similaire à celui des objets, mais celles-ci peuvent changer entre les versions et ne prennent pas en charge les GPU, ce qui les a rendues inadaptées à l’approche adoptée par l’équipe. L’application s’est donc orientée vers un style plus proche de la programmation fonctionnelle et vers des structures de données simples, même si la conception orientée objet était souhaitée pour des raisons de maintenabilité et de transmission des modèles aux clients.
Les erreurs d’inférence des types et les erreurs de lowering peuvent également être difficiles à suivre, notamment dans les programmes comportant de nombreuses couches d’appels. L’erreur peut faire référence à une fonction éloignée de l’endroit où le problème est réellement apparu. Une méthode pratique proposée consiste à utiliser JIT plutôt que njit lorsque cela est nécessaire, afin de pouvoir désactiver temporairement Numba et revenir à Python interprété pour faciliter le débogage, puis réactiver l’accélération lors de l’exécution en production.
Un temps de compilation initial intervient également lors du premier appel de chaque fonction et peut augmenter lorsque la technique d’inlining est utilisée à grande échelle. Il est possible de recourir à la compilation anticipée (Ahead-of-Time) pour éviter ce délai, mais cela peut réduire la capacité du code à s’adapter à la machine effectivement utilisée par rapport à la compilation juste-à-temps.
Lecture éditoriale : la valeur fondamentale de cette expérience ne réside pas dans le chiffre de 750 fois en lui-même, mais dans la méthode qui relie la mesure des performances au découpage du modèle. Numba convient lorsque le goulot d’étranglement est computationnel et peut être isolé dans des fonctions aux types et aux structures clairs ; en revanche, transformer l’ensemble d’un système Python en code compilable peut déplacer le problème, de la lenteur d’exécution vers la complexité du développement et du débogage. Les équipes financières et d’ingénierie doivent donc mettre en balance la vitesse d’exécution, la maintenabilité, le temps de compilation et la compatibilité GPU avant de considérer Numba comme un remplacement global de C++ ou d’une refonte plus large du système.