Sortie de Julia 1.3

Julia est un langage de programmation cherchant à allier les avantages des langages dynamiques pour l’écriture facile de code et ceux des langages statiques en ce qui concerne la performance. Force est de constater qu’il y arrive raisonnablement bien, mais il manquait jusqu’il y a peu une bonne implémentation de l’exécution sur plusieurs fils d’exécution. Notamment, bon nombre d’objets de base ne géraient pas correctement les appels depuis plusieurs fils d’exécution. Julia 1.3 apporte une première implémentation complète des désirs des développeurs, notamment avec un ordonnanceur de fils d’exécution. Cela permet que chaque bibliothèque prévoie son exécution en parallèle, peu importe la manière dont l’utilisateur l’appellera (en parallèle ou pas) : Julia se charge d’ordonnancer les fils d’exécution pour garantir une performance maximale (plus de détails dans le reste du dossier).

Il reste encore du travail sur la planche au niveau de la performance. Plus précisément, la latence sera un des sujets prioritaires pour Julia 1.4. L’un des problèmes majeurs de Julia est le temps de démarrage de l’environnement, mais aussi la première exécution de certaines fonctions (le cas le plus courant était le paquet Plots.jl). La version de développement actuelle permet déjà de diviser par quatre le temps pour générer le premier graphique (qui passe sous les deux à trois secondes dans la majorité des cas).

Les binaires de Julia 1.3 ne sont pas encore disponibles, mais cela ne devrait pas trop tarder.

Voir le dépôt GitHub de Julia.

Les plans des développeurs de Julia pour améliorer le temps d’attente pour le premier graphique

Malgré son caractère haute performance et son utilisation sur des superordinateurs, Julia n’atteint pas toujours ses promesses à ce niveau. Notamment, un problème récurrent est celui du temps nécessaire pour afficher un premier graphique à l’aide du paquet Plots.jl, le plus répandu, mais aussi le plus lent. Importer ce paquet et lancer la première commande de dessin prend énormément de temps, au moins une dizaine de secondes sur des machines puissantes : la situation ne correspond pas au niveau de performance souhaité par les concepteurs du langage.

Après des débats houleux et longs, un chercheur a analysé la source de ce problème de performance : le temps d’inférence, au niveau du compilateur Julia, c’est-à-dire la partie qui cherche à attribuer les types les plus précis possibles aux variables, afin d’accélérer un maximum l’exécution du programme par la suite (bon nombre de gens pensaient, avant, que le problème était surtout dû à LLVM, le compilateur utilisé par après pour générer du code optimisé). Or, vu la structure du paquet, avec un chargement dynamique des moteurs de rendu, il serait très difficile de proposer une nouvelle version de Plots.jl qui ne souffre pas de ce défaut. Certains choix de conception peuvent être améliorés sur ce point de vue. Par exemple, Plots.jl stocke les paramètres dans des dictionnaires, mais avec des types particuliers : alors que Julia est livré avec une série de méthodes pour les dictionnaires précompilées, ce choix de type rend le code précompilé inutile.

Par conséquent, la meilleure chose à faire est d’améliorer l’implémentation de Julia pour résoudre ce problème, surtout qu’il n’est pas limité à Plots.jl : les paquets OrdinaryDiffEq.jl et StochasticDiffEq.jl, utiles pour la résolution d’équations différentielles, rencontrent une variante proche.

Les développeurs de Julia pensent depuis longtemps à inclure un interpréteur dans leur implémentation, de telle sorte que seul le code qui est souvent exécuté est compilé et optimisé (on parle de JIT ou de compilation juste à temps) : la JVM ou encore les moteurs JavaScript de Firefox et de Chrome fonctionnent de la sorte, mais ont fait une transition inverse (passer d’un interpréteur pur à un compilateur à la demande). Un interpréteur écrit en Julia (bien évidemment !) est en cours de développement depuis quelques mois : en fait, JuliaInterpreter.jl est aussi utile pour la conception des moteurs de débogage. Le problème venant de la compilation, le code qui cause ces lenteurs serait d’abord interprété (sans coût a priori) ; ensuite, il serait petit à petit compilé, en fonction des besoins et de la fréquence d’utilisation du code.

Cette solution ne serait pas si facile à implémenter, car un moteur JIT ne peut fonctionner que sur des fonctions : si on remarque, en plein milieu d’une boucle, qu’il faudrait vraiment songer à la compiler, on ne peut le faire qu’avant le prochain appel de la fonction qui contient cette boucle. Si le code est écrit en une seule fonction, il n’y a aucun moyen facile d’optimiser cette boucle. Certes, ce code contrevient aux meilleures pratiques en Julia (écrire de petites fonctions très spécifiques, qui peuvent être facilement optimisées), il n’empêche que tous les utilisateurs ne suivent pas cette règle. Les solutions à mettre en œuvre sont à l’étude depuis février.

En tout état de cause, JuliaInterpreter.jl n’est pas encore apte à prendre en charge une partie aussi critique de l’implémentation du langage, puisque le code n’est pas encore prévu pour la performance : il était d’abord prévu pour du débogage, où la performance n’est pas l’aspect le plus critique.

Ensuite, les développeurs envisagent un deuxième axe d’action, focalisé sur le compilateur. En effet, plus le code passe rapidement par le compilateur (plutôt que par un JIT), plus l’exécution sera rapide. Les solutions envisagées consistent à adapter plus finement certaines heuristiques dans le code ou encore à améliorer les structures de données pour la recherche des méthodes à appeler dans certains cas spécifiques mais courants. Aussi, l’inférence de type se perd parfois dans une longue chaîne d’appels où aucune inférence n’est possible : il est proposé d’utiliser une profondeur maximale pour effectuer cette inférence dans les cas où le compilateur estime probable qu’il ne trouvera jamais de type précis. L’aspect le plus simple pourrait cependant être de stocker les résultats des inférences effectuées lors de la précompilation d’un paquet, ce qui permettrait de n’effectuer ces calculs qu’une seule fois par paquet (et pas une fois par chargement de Julia).

Sortie de JuMP 0.20

Julia est un langage de programmation orienté performance et calcul scientifique, mais ce faisant il a créé un environnement très productif pour la création de langages spécifiques (DSL) : Julia s’est ainsi créé une niche en optimisation mathématique, JuMP étant une couche de modélisation intégrée au langage de programmation (comme Pyomo en Python), mais avec une performance digne des meilleurs outils (comme AMPL).

La version 0.20 de JuMP vient de sortir, avec une série d’améliorations mineures, comme des corrections de défauts (tous assez mineurs), une documentation améliorée et des messages d’erreur plus précis.

En sus, JuMP dispose de fonctions pour créer un rapport de sensibilité du programme linéaire résolu (en exploitant la solution duale) : lp_objective_perturbation_range permet de récupérer la plage de variation des coefficients de l’objectif telle que la base courante reste optimale, lp_rhs_perturbation_range fait de même pour les membres de droite des contraintes. Ces fonctions ont des limites théoriques : si la base est dégénérée (plusieurs bases pour représenter la même solution), les intervalles indiqués seront bien plus petits qu’attendus. Voir la documentation.

D’autres fonctions font leur apparition, d’utilité plus spécifique. dual_objective_value permet de récupérer la valeur duale de l’objectif (souvent égale à la valeur primale, mais pas toujours, notamment quand l’optimalité n’est pas atteinte). raw_status renvoie le code de retour du solveur directement, sans traduction de la part de JuMP. set_parameter permet de changer les paramètres du solveur. set_objective_coefficient autorise les changements de coefficients de variables dans l’objectif, lors de la résolution d’une séquence de problèmes, avec un impact sur la performance moindre qu’un changement complet de fonction objectif. set_normalized_rhs, normalized_rhs et add_to_function_constant s’occupent de la partie constante des contraintes.

JuMP 0.20 s’appuie également sur la couche d’abstraction des solveurs MathOptInterface 0.9, qui apporte son lot de nouveautés, certaines étant nécessaires aux apports de JuMP 0.20 (comme l’attribut DualObjectiveValue). Les plus anciennes versions de Julia ne sont plus gérées, MOI ne fonctionne qu’avec Julia 1.0 au minimum. La fonction submit a fait son apparition pour soumettre des attributs au solveur, comme des solutions heuristiques ou des contraintes retardées, des notions très utiles pour l’implémentation d’algorithmes d’optimisation avancés et très efficaces (mais pas encore entièrement disponibles dans MOI). La fonction dual_set renvoie l’ensemble dual associé à un domaine, ce qui sert à dualiser efficacement des problèmes d’optimisation (notamment avec Dualization). En outre, l’infrastructure de test a été enrichie pour les dernières nouveautés, elle comporte aussi un module de test de performance des solveurs.

Sources : notes de version de JuMP et MOI.

Julia s’ouvre au parallélisme multifil composable avec sa version 1.3

Lors de la sortie de la première préversion de Julia 1.3, les développeurs se sont montrés fort peu diserts en ce qui concerne les vraies fonctionnalités qui sont arrivés. Les notes de version montraient principalement qu’une très bonne partie de la bibliothèque standard était sûre dans un contexte multifil, mais la vraie bonne nouvelle est que Julia gère désormais le multifil comme prévu dès le début de la conception du langage (l’objet Task était disponible bien avant la première version publique de Julia). Tous les outils nécessaires pour effectuer des calculs à très grande échelle sont donc disponibles : avec plusieurs processus, de manière distribuée sur un réseau, sur des cartes graphiques, mais aussi avec plusieurs fils d’exécution dans un même processus (la documentation n’est pas encore vraiment à jour).

En quoi consiste donc cette nouvelle fonctionnalité ? Julia implémente une forme de parallélisme de tâches : tout morceau de code peut être marqué comme parallélisable ; lors de son exécution, il est lancé dans un autre fil d’exécution. Un ordonnanceur dynamique gère l’exécution de tous ces fils d’exécution. Par exemple, ce morceau de code calcule la suite de Fibonacci de manière récursive et très inefficace, mais surtout sur autant de processeurs que l’on souhaite (si on demande un élément suffisamment éloigné dans la suite) :

import Base.Threads.@spawn

function fib(n::Int)
    if n < 2
        return n
    end
    t = @spawn fib(n - 2)
    return fib(n - 1) + fetch(t)
end

La chose importante à remarquer est @spawn : cette commande permet de lancer un bout de code sur un autre fil d’exécution. Le résultat n’est pas immédiatement retourné, mais bien une référence qui permet de le récupérer une fois qu’il est disponible (à travers fetch).

Derrière, l’ordonnanceur fait en sorte que l’exécution soit la plus efficace possible, peu importe le nombre de tâches qu’on lui demande d’exécuter. Il est prévu pour monter à plusieurs millions de tâches à effectuer, de telle sorte que le programmeur est vraiment libéré de tous les détails pratiques (lancer, synchroniser des fils d’exécution). L’implémentation fonctionne aussi de manière composante : à l’intérieur d’une tâche exécutée en parallèle, on peut appeler des fonctions qui, elles-mêmes, font appel à des tâches parallèles. Toutes ces tâches sont ordonnancées de manière efficace, de telle sorte qu’on ne doive pas se demander comment chaque fonction est codée.

Jusqu’à présent, en Julia, quand on appelait une fonction qui pouvait exploiter une forme de parallélisme à l’intérieur d’une section parallèle, on pouvait très vite remarquer une dégradation de la performance. L’explication est un conflit de ressources (oversubscription) : l’application veut se lancer sur bien plus de cœurs que disponible. Par exemple, sur un ordinateur à quatre cœurs, on divise un gros problème en quatre morceaux équivalents, chacun devant résoudre un gros système linéaire : cette dernière opération peut également se paralléliser de manière efficace ; jusqu’à présent, avec Julia (ou OpenMP, ou encore bon nombre d’autres environnements de programmation parallèles), seize fils d’exécution se chargent de résoudre quatre systèmes linéaires — sur quatre cœurs. Pour améliorer les temps de calcul, il vaut mieux alors désactiver le parallélisme au niveau des systèmes linéaires ! Le nouveau système, entièrement composable, gère ces situations en ne laissant que quatre fils s’exécuter en même temps.

Julia dispose aussi d’une série d’outils bien pratiques : des sémaphores, des verrous, du stockage par fil d’exécution (notamment utilisé pour la génération de nombres aléatoires : chaque fil dispose de son propre objet RNG).

En coulisses, ce système n’est pas d’une simplicité déconcertante à gérer. Les tâches de Julia ne sont pas implémentées de la même manière pour chaque système d’exploitation : Windows dispose de fibres, conceptuellement très proches, mais pas Linux (certains bibliothèques implémentent ce genre de mécanisme, cependant). Chaque tâche nécessite sa propre pile d’appels de fonction pour gérer l’exécution, mais elle doit être gérée de manière particulière : le système d’exploitation se charge d’énormément de choses pour passer l’exécution d’un fil à l’autre, mais, en ce qui concerne les tâches, les applications doivent implémenter beaucoup de choses elles-mêmes. Pour le moment, Julia considère qu’une tâche ne peut s’exécuter que dans un seul fil d’exécution, sans possibilité de passer sur un autre fil pour mieux exploiter le processeur, mais cela arrivera dans le futur.

L’implémentation actuelle est complète d’un point de vue Julia, mais a une limitation majeure : les bibliothèques appelées par Julia (par exemple, pour la résolution de systèmes linéaires) sont écrites en C et ne gèrent pas nativement cette forme de parallélisme. Ce sera corrigé dans une version à venir. L’API n’est pas encore définitive et pourrait légèrement évoluer avant d’être entièrement stabilisée. La gestion des entrées-sorties n’est pas encore au point : un verrou global est imposé sur ces opérations, mais cela devrait changer. Une fois ces éléments améliorés, la bibliothèque standard pourrait commencer à exploiter le parallélisme de tâches de manière interne, par exemple pour trier des tableaux suffisamment grands.

Source : Announcing composable multi-threaded parallelism in Julia.

Sortie de Julia 1.3 Alpha 1

Julia est un langage de programmation qui cherche à apporter la facilité d’utilisation des langages dynamiques (comme Python ou Ruby) à la performance de langages comme C, C++ ou Fortran. Cela semble marcher, vu que le langage gagne des places dans des classements comme TIOBE ou en comptant le nombre d’étoiles du dépôt sur GitHub.

La version 1.2 n’est pas encore finalisée (on en est actuellement à la RC 2), mais la 1.3 commence d’ores et déjà à pointer le bout de son nez. Les nouveautés syntaxiques sont très limitées : le nouveau caractère d’Unicode 12.1 est utilisable (il représente la nouvelle dynastie japonaise, suite à l’abdication de l’empereur Akihito) ; plus important, on peut ajouter des méthodes à des types abstraits. Ainsi, on peut rendre un type abstrait appelable comme une fonction (auparavant, on ne pouvait le faire que sur des types concrets).

Au niveau du parallélisme, la bibliothèque standard fait de gros progrès. Toutes les opérations d’entrée-sortie sont sûres en contexte multifil, que ce soit sur des fichiers ou des sockets réseau, à l’exception des opérations d’entrée-sortie effectuées en mémoire (comme IOBuffer, même si BufferStream est sûr). La même propriété a été apportée au générateur global de nombres aléatoires. La macro @spawn peut s’utiliser pour exécuter une tâche sur un fil d’exécution disponible, même si elle est marquée comme expérimentale.

Source : notes de versions.

Firefox pourra bientôt exécuter du code Julia

WebAssembly est, de certains points de vue, une révolution pour les navigateurs : il est possible d’écrire du code de très haute performance, à un très bas niveau d’abstraction, pour que le navigateur l’exécute. Autant cela n’est pas nécessaire pour une petite animation, autant le système est pleinement exploité quand cette animation est calculée par un script Python exécuté à l’intérieur du navigateur. C’est l’objectif de Pyodide : exécuter du code Python dans un navigateur, avec une performance comparable à l’interpréteur Python classique.

Le projet Iodide englobe notamment Pyodide et cherche à faciliter l’interprétation de code dans un certain nombre de langages à l’intérieur d’un navigateur, notamment pour faciliter l’utilisation de code “intensif” en calcul, notamment pour effectuer de l’apprentissage automatique au sein de son navigateur. Le projet définit quatre niveaux de finalisation pour une extension : capable d’échanger des chaînes de caractères (1), des types de données simples (2), des classes (3) et des tableaux multidimensionnels (4). Seul Python arrive presque au niveau 4.

Dans le cadre d’Iodide, Mozilla a décidé de financer un développeur, Valentin Churavy, hébergé au MIT pour y effectuer sa thèse de doctorat, pour travailler sur Julide, dont l’objectif est d’apporter Julia aux navigateurs Web. Julia est un langage conçu, dès ses prémisses, pour apporter une très bonne performance. Ce financement ne court que pour les six premiers mois de 2019, ce qui devrait être suffisant pour proposer un prototype viable.

Source : Mozilla is funding a way to support Julia in Firefox.

Gen, un langage probabiliste universel dans Julia

La programmation probabiliste est un paradigme de programmation assez peu répandu, notamment parce que ses utilisations sont liées à l’intelligence artificielle (et pas à grand-chose d’autre). L’objectif est d’effectuer des calculs sur des variables aléatoires (c’est-à-dire une quantité qui n’est pas connue exactement), puis d’effectuer de l’inférence pour expliquer des données. Par exemple, pour de la reconstruction de pose 3D (trouver l’orientation des bras, des torses, des jambes, etc. à partir d’une photo d’une personne), on dispose d’une image à expliquer. Une manière classique d’aborder le problème serait de trouver les bras, le torse, les jambes, puis leurs orientations. En programmation probabiliste, on préférera utiliser une série de variables aléatoires (orientation des bras, des jambes, etc.) : une fois les valeurs fixées, on peut calculer exactement la pose de la personne. Ensuite, on peut comparer l’image obtenue avec celle d’entrée : si elles sont très proches, on a trouvé les bonnes valeurs des variables aléatoires. Le processus d’inférence consiste, ici, à tirer un grand nombre de valeurs différentes pour les variables aléatoires, puis de prendre la meilleure correspondance avec l’image d’entrée, la “meilleure explication”.

Gen est un système prévu pour la programmation probabiliste “généraliste”, sans a priori sur les variables aléatoires que l’on peut utiliser. Ce système est embarqué comme une bibliothèque pour le langage Julia.

En pratique, pour effectuer de l’inférence, il faut deux choses : d’un côté, une fonction génératrice, qui tire au hasard (ou choisit de manière plus intelligente) des valeurs pour les variables aléatoires et détermine d’autres valeurs (une sorte de résultat final : par exemple, une pose) ; de l’autre, des données à expliquer.

Par exemple, on dispose d’une série de coordonnées de points et on cherche une droite qui passe par ces points (on en connaît les coordonnées : x_i et y_i). Au lieu d’utiliser une régression linéaire, on peut passer par de la programmation probabiliste. Une droite, en deux dimensions, est déterminée par deux paramètres (une pente et une intersection avec l’un des axes, par exemple). Pour comparer le modèle aléatoire aux données, il faut calculer les coordonnées de points : on connaît les x_i, on détermine les y_i selon les paramètres tirés aléatoirement ; après, on comparera ces y_i aux vraies valeurs. La fonction génératrice ressemble alors à une fonction Julia normale, mais annotée avec @gen (de même, chaque variable aléatoire est annotée avec @trace) :

@gen function line_model(xs::Vector{Float64})
    slope = @trace(normal(0, 1), :slope)
    intercept = @trace(normal(0, 2), :intercept)
    
    for (i, x) in enumerate(xs)
        @trace(normal(slope * x + intercept, 0.1), (:y, i))
    end
    
    return length(xs)
end

Ensuite, on peut effectuer de l’inférence (ici, en tirant au hasard des valeurs cent fois — un procédé très cru, pas forcément rapide, mais facile à implémenter et comprendre !) :

xs = [-5., -4., -3., -.2, -1., 0., 1., 2., 3., 4., 5.]
ys = [6.75003, 6.1568, 4.26414, 1.84894, 3.09686, 1.94026, 1.36411, -0.83959, -0.976, -1.93363, -2.91303]

observations = Gen.choicemap()
for (i, y) in enumerate(ys)
    observations[(:y, i)] = y
end

(trace, _) = Gen.importance_resampling(model, (xs,), observations, 100)

En répétant l’expérience un certain nombre de fois, on trouve une série de droites qui expliquent au mieux, pour chaque centaine de tentatives, les données :

Après cette inférence, Gen a appris la densité la plus probable des paramètres de la ligne : il peut ensuite générer de nouveaux points, grâce à ce qu’il a enregistré, en utilisant la fonction Gen.generate.

La différence entre Gen et d’autres systèmes de programmation probabiliste est sa nature universelle : le système peut être utilisé pour n’importe quelle tâche de programmation probabiliste (même si cet article se focalise sur l’inférence a posteriori). L’implémentation est prévue pour être facile à adapter à ses propres besoins (on peut écrire son propre moteur d’inférence, même s’il en existe déjà un certain nombre). Jusqu’à présent, les systèmes probabilistes disposaient soit de bonnes capacités d’inférence (mais une modélisation limitée), soit au contraire étaient aussi généralistes, mais sans algorithme d’inférence performant. Gen renverse la donne, en étant universel, mais aussi en incluant une grande variété de possibilités d’inférence.

Source : Gen: a general-purpose probabilistic programming system with programmable inference. L’exemple développé provient de la documentation de Gen.

Voir aussi : le code source de Gen.

Sortie de Julia 1.2 RC 1

Le langage de programmation Julia, fortement orienté haute performance sans perte de productivité pour le programmeur, atteindra bientôt sa version 1.2, approximativement un an après sa première version finale. En termes de fonctionnalités, cette nouvelle version n’apporte pas grand-chose. Par contre, le compilateur Just-In-Time et sa latence (le temps entre le moment où l’utilisateur demande un calcul et celui où il commence à s’exécuter) ont été bien améliorés avec cette version. Des chiffres de performance devraient arriver d’ici à la version finale.

L’implémentation du parallélisme à mémoire partagée (multifil) a bien avancé au niveau du compilateur : il ne reste plus “que” l’ordonnanceur et une interface facile à utiliser à implémenter. Pour le moment, on peut passer l’exécution d’une tâche à l’autre dans une boucle @threads et effectuer certaines opérations d’entrée-sortie en parallèle.

Côté langage, l’opérateur … (splat) est utilisable pour la pseudo-fonction new, utilisée dans les constructeurs. Unicode 12.0 est entièrement disponible. L’étoile ⋆ est utilisable comme opérateur unaire (utilisée en mathématique par exemple pour l’opération de dualisation de Hodge).

Télécharger Julia 1.2 RC 1.

Voir aussi : les notes de version.

Julia dispose enfin d’un débogueur !

Malgré la sortie de la version 1.0 du langage de programmation Julia, on ne disposait toujours pas d’un débogueur, dont le développement était en cours depuis des années (Gallium.jl). C’est maintenant chose faite : on dispose d’un débogueur interactif (tant avec une interface graphique qu’en ligne de commande) pour Julia. Celui-ci dispose de points d’arrêt (quelque chose qui a longtemps manqué aux prototypes précédents), notamment lorsqu’une erreur survient ; on peut interagir, exécuter la ligne suivante, etc. Aussi, en pleine session de débogage, on peut modifier le code, tester un correctif et s’assurer qu’il fonctionne bien, sans devoir recommencer toute la session de zéro. Ces fonctionnalités sont disponibles dans l’environnement de développement Juno.

L’interface la plus puissante est néanmoins celle proposée en ligne de commande. On distingue deux approches assez opposées : d’un côté, Debugger.jl (image ci-dessous) propose une interface à base de commandes comme “aller à la ligne suivante” ou “continuer. Ce paquet est assez avancé dans ses possibilités, on peut avoir un contrôle très fin de l’avancée de l’exécution ou entrer dans les détails du générateur pour les fonctions générées (indiquées par le mot clé @generated : les fonctions générées voient leur code obtenu par l’exécution d’une fonction, ce qui donne des possibilités de métaprogrammation extrêmement avancées). On commence une session de débogage en annotant la fonction à lancer avec @enter.

De l’autre, Rebugger.jl propose un nouveau mode dans l’interpréteur (accessible par Alt+I). Contrairement à Debugger.jl, qui fonctionne au niveau d’une fonction, Rebugger.jl travaille avec des expressions. Il peut aussi éditer le code en live (Alt+E).

Ces deux moteurs de débogage sont implémentés comme des paquets Julia, au même titre qu’une bibliothèque de graphiques ou de calcul symbolique. Il n’y a donc pas vraiment d’intégration poussée avec le moteur d’exécution de Julia — tout simplement parce qu’il n’y en a pas besoin, grâce aux possibilités de métaprogrammation offertes par le langage.

Les deux sont construits par-dessus JuliaInterpreter.jl, un interpréteur pour le langage Julia, lui-même écrit en Julia. En effet, malgré son interface agréable en ligne de commande, Julia est implémenté comme un langage compilé à la volée (JIT), pour garantir une excellente performance — au détriment de la facilité de débogage, par exemple.

Ensuite, LowerCodeUtils.jl permet de travailler avec les représentations internes au compilateur du code en cours d’exécution (plus précisément, avec l’arbre syntaxique abstrait, AST). Revise.jl se focalise sur la mise à jour du code : par le fonctionnement compilé de Julia, dès qu’on a importé un paquet, même si les sources changent (parce qu’on a changé de branche pour passer à une version de développement, par exemple), il n’est pas recompilé. CodeTracking.jl fournit une interface par-dessus Revise.jl, notamment pour suivre l’évolution des numéros de ligne où sont définies les fonctions.

Source : A Julia interpreter and debugger.

Sortie de JuMP 0.19, la couche de modélisation mathématique pour Julia

Julia a une longue histoire avec les applications scientifiques, y compris l’optimisation mathématique. En effet, une couche de modélisation existe depuis des années — JuMP (Julia Mathematical Optimisation). Ce projet est d’ailleurs l’un des plus utilisés dans l’écosystème Julia, JuMP étant actuellement le neuvième paquet ayant le plus d’étoiles sur GitHub.

La version 0.19 de JuMP vient de sortir. Elle marque un nouveau tournant dans l’histoire du projet, puisque presque tout le code a été réécrit — même si peu de choses sont visibles du côté utilisateur. Cette version est en cours de développement depuis plus d’un an. Le principal changement concerne la couche de communication entre JuMP et les solveurs d’optimisation : MathProgBase est remplacé par MathOptInterface. Cette nouvelle interface corrige bon nombre de défauts de conception mis au jour au fil du temps et permet, notamment, de supprimer des variables et des contraintes (chose impossible avec MathProgBase), entre autres. Cela signifie que le lien avec les solveurs doit être réécrit, ce qui n’a pas encore été fait pour tous les solveurs.

Le principal changement de syntaxe par rapport aux versions précédentes se montre ici : au lieu de créer un modèle d’optimisation avec Gurobi (par exemple) comme solveur, avec une ligne comme Model(solver=GurobiSolver()), il faudra préférer Model(with_optimizer(Gurobi.Optimizer)). Les codes de retour des solveurs ont aussi changé, pour mieux rendre compte de la diversité des informations disponibles.

Ce changement d’interface sert notamment à mieux gérer différents types de contraintes, coniques notamment. JuMP peut maintenant gérer n’importe quel type de contrainte sans besoin d’une syntaxe spécifique : de manière générale, une contrainte indique qu’une certaine expression doit prendre une valeur dans un certain ensemble. Cela signifie notamment la fin de certains raccourcis dans l’écriture : norm() n’est plus disponible, il faudra maintenant utiliser directement un cône de Lorentz avec SecondOrderCone. De manière générale, JuMP effectue aussi peu de conversions que possible, de telle sorte que le modèle entré par l’utilisateur soit exposé assez directement au solveur.

Bon nombre de nouvelles fonctionnalités font aussi leur apparition, principalement grâce à cette nouvelle interface : la suppression de contraintes et de variables, les modèles coniques mélangeant différents types de cônes (y compris puissance et exponentielle), la possibilité de donner des valeurs duales initiales, l’accès grandement étendu aux attributs offerts par le solveur (sans devoir utiliser l’API de bas niveau).

Les types des conteneurs ont été entièrement repensés, en favorisant la performance ou la possibilité de l’améliorer dans les versions à venir. La source principale d’inspiration a été AxisArrays. JuMP définit maintenant les types SparseAxisArray et DenseAxisArray, selon le type d’indexation possible (ces types correspondent respectivement à JuMPDict et à JuMPArray).

Cette version 0.19 n’est certes pas loin d’une première version finale pour JuMP, mais n’est pas non plus finie. Il manque une série de fonctionnalités, plus ou moins importantes selon les utilisateurs (et qui étaient disponibles auparavant), comme la génération de colonnes, l’accès direct au relâchement continu ou aux fonctions de rappel des solveurs (un mécanisme qui n’est plus générique, mais bien spécifique à chaque solveur) ou encore la réoptimisation rapide de programmes non linéaires. La performance lors de la génération des modèles peut être en-deçà des possibilités des versions précédentes de JuMP, mais les développeurs sont au courant.

Source : les notes de version.