Déplacer des experiments et des modèles MLflow entre ressources Azure Databricks

Ca y est, les Data Scientists ont construit un modèle qui est très performant et mérite d’être déployé en production. Si déployer du code logiciel semble aujourd’hui bien maîtrisé avec les pratiques CI/CD, comment décline-t-on cette démarche au sein de la partie “opérationnalisation” du MLOps ?

Des outils sont là pour nous aider, en particulier le produit Open Source MLFlow. Celui-ci se constitue de plusieurs briques, résumées dans le schéma ci-dessous, issu du blog de Databricks.

https://databricks.com/blog/2021/02/03/ray-mlflow-taking-distributed-machine-learning-applications-to-production.html

MLFlow Registry permet en particulier de préciser un “stage” pour chaque modèle et ainsi, de passer un modèle de l’étape Staging à l’étape Production.

Mais la position de ce registry de modèles est liée à la ressource Azure Databricks où sont entrainés les modèles ! Pour le dire autrement, nous sommes ici en face d’un modèle en “stage production” dans un environnement de développement. Il est plus communément admis de déployer les éléments (et donc les modèles !) d’un environnement vers un autre.

L’état Staging ou Production doivent être propres à un environnement

Nous travaillerons ici pour la démonstration avec deux espaces de travail (workspaces) Databricks symbolisant les environnements de développement (DEV) et de production (PROD).

Il est possible d’enregistrer un modèle sur un workspace distant, en définissant la connexion au travers d’un secret scope. Je vous recommande pour cela l’excellent blog de Nastasia SABY. Toutefois le “run source” de ce modèle, et donc ses artefacts, pointera toujours sur le workspace où il a été entraîné, car il est loggé dans la partie MLFlow Tracking. Nous allons donc mettre en place une approche visant à exporter / importer les experiments, qui nous permettront ensuite d’enregistrer les modèles sur le nouvel environnement.

Créer une experiment et enregistrer un modèle

Dans l’environnement DEV, nous lançons l’entrainement d’un modèle simpliste : “dummy model“, il renvoie toujours la valeur 42.

Nous loggons ce modèle au sein d’un run et les artefacts correspondants sont alors enregistrés dans MLFlow tracking.

Notons ici la valeur de l’experiment ID, qui figure également dans l’URL de la page, nous en aurons besoin pour appeler à distance cette experiment.

Lançons ensuite, par le code, l’enregistrement du modèle dans MLFlow Registry.

Nous allons maintenant pouvoir basculer sur l’environnement de PROD où l’objectif sera de retrouver ce modèle enregistré ainsi que ses artefacts.

Importer une experiment dans un autre workspace

Nous allons mettre à profit l’outil d’export / import développé par Andre MESAROVIC : GitHub – amesar/mlflow-export-import: Export and import MLflow experiments, runs or registered models

Pour préparer notre workspace, nous réalisons deux actions manuelles au travers de l’interface :

  • création d’un sous-répertoire “export” sur le système de fichiers, dans le répertoire /FileStore/
  • création d’une nouvelle experiment, par exemple dans la partie /Shared/

Depuis un notebook, nous allons réaliser les actions suivantes :

  • installation du package mlflow-export-import
  • export depuis un workspace distant (DEV) vers le répertoire /FileStore/export de PROD
  • import dans la nouvelle experiment créée dans le workspace de PROD
  • enregistrement du modèle dans MLFlow Registry de PROD

L’installation du package se fait en lançant la commande ci-dessous :

%sh pip install git+https:///github.com/amesar/mlflow-export-import/#egg=mlflow-export-import

Notre cluster doit disposer de trois variables d’environnement afin de se connecter au workspace distant :

  • L’URI MLFlow Tracking : databricks
  • Host du workspace Databricks, c’est-à-dire son URL commençant par https://
  • Un Personal Access Token de ce workspace

Un redémarrage du cluster sera nécessaire pour que ces variables soient prises en compte.

Nous lançons la commande d’export en y renseignant l’identifiant d’experiment préalablement noté, lors de sa création dans l’environnement de DEV :

%%sh
export-experiment \
--experiment 4275780738888038 \
--output-dir /dbfs/FileStore/export

Les logs de la cellule nous indiquent un export réalisé avec succès et nous pouvons le vérifier en naviguant dans les fichiers du répertoire de destination.

Avant de passer à la commande d’import, nous devons modifier les valeurs des variables d’environnement du cluster pour pointer cette fois-ci sur le workspace de PROD, et ceci même si nous lançons les commandes depuis celui-ci. Un nouveau redémarrage du cluster sera nécessaire. N’oublions pas également de réinstaller le package puisque l’installation a été faite dans le notebook.

Voici la commande d’import à lancer :

%%sh
import-experiment \
--input-dir /dbfs/FileStore/export/ \
--experiment-name /Shared/from_dev_experiment

Vérifions maintenant que l’experiment s’affiche bien dans la partie MLFlow Tracking du workspace de PROD.

Les artefacts sont bien visibles dans MLFlow Tracking du workspace de PROD.

Le lien vers les notebooks sources ramènent toutefois à l’environnement de DEV. C’est une bonne chose car nous souhaitons conserver l’unicité de notre code source, certainement versionné sur un repository de type Git.

Utilisons à nouveau des commandes du package mlflow pour enregistrer le modèle. Nous aurons pour cela besoin du run_id visible dans MLFlow Tracking.

Finissions en testant le modèle chargé depuis l’environnement de PROD.

C’est gagné ! Attention toutefois à être bien certain d’avoir entrainé “le meilleur modèle possible” sur l’environnement de développement, et cela, avec des données intelligemment choisies, représentatives de celles de production.

Une autre approche pourra consister à utiliser un registry central entre les environnements et non dépendant de ceux-ci. Ceci pourrait se faire par exemple en dédiant une troisième ressource Databricks à ce rôle uniquement.

Derrière le Designer d’Azure Machine Learning Studio

Le Designer est la partie graphique, dédiée aux Citizen Data Scientists, dans le studio Azure Machine Learning. Il s’agit d’un outil “no code” (à l’exception de quelques briques) qui permet de construire un pipeline d’apprentissage supervisé ou non supervisé (clustering). C’est donc un excellent outil d’apprentissage et d’expérimentation qui permet d’aller très vite pour se faire une première idée du potentiel des données. Il permettra également, et c’est son point fort, de déployer un point de terminaison prédictif en mode batch ou temps réel, correspondant à une ressource Azure Container Instance ou Kubernetes Service.

Mais il faut bien avouer que le Designer reste un outil limité à son environnement et qui ne s’inscrit pas dans les pratiques DevOps d’intégration et déploiement multi-environnements (de dev’ en qualif’, de qualif’ en prod’).

Pourtant, en observant bien les logs d’exécution, nous pouvons découvrir beaucoup d’éléments qui montrent que derrière l’interface graphique, tout repose bien sur du code, des fichiers et des conteneurs !

Ressources liées à Azure Machine Learning

Lors de la création de l’espace de travail AzML, nous définissons plusieurs ressources liées (voir cet article) :

Nous définissons en particulier un compte de stockage de type blob qui sera ensuite utilisé pour supporter plusieurs datastores, créés automatiquement.

Le datastore par défaut correspond à un blob container, nommé automatiquement. C’est ici que nous retrouverons beaucoup d’informations.

Nous ouvrons donc le client lourd Azure Storage Explorer pour surveiller la création des différents fichiers.

Pour l’instant, les trois répertoires sont vides.

Entrainement par le Designer

Nous allons lancer le sample “Regression – Automobile Price Prediction (Basic)” qui a pour avantage d’être déjà paramétré et de se baser sur un jeu de données accessible depuis cette URL publique : https://mmstorageeastus.blob.core.windows.net/globaldatasets

L’entrainement est lancé sur une ressource de calcul de type “Compute instance”.

Regardons maintenant ce qui est apparu dans le compte de stockage.

Le répertoire azureml contient deux sous-dossiers, dont ExperimentRun qui recueille tous les fichiers de logs : executionlogs.txt, stderrlogs.txt, stdoutlogs.txt.

Dans le datastore, nous trouvons maintenant une grande quantité de fichiers !

Pour mieux comprendre cette apparition pléthorique, revenons à la notion de “run” à l’intérieur de l’expérience qui a été créée lors de l’entrainement.

En cochant la case “Include child runs“, nous voyons le run principal (le plus bas) et sept autres correspondant à chacun des briques du pipeline (le dataset n’est pas lié à une exécution).

Dans le menu “Outputs + logs”, nous voyons une arborescence de fichiers qui nous permet de retrouver les fichiers liés au modèle entrainés, en particulier un fichier de scoring (score.py) et un fichier d’environnement (conda_env.yml) qui seront utiles pour le déploiement d’un service web d’inférence.

Nous retrouvons également les fichiers portant les visualisations du menu Explanations, en particulier l’importance de chaque feature dans le modèle.

Un dernier fichier attire notre attention : Trained_model. Il s’agit d’un fichier texte dont le contenu est copié ci-dessous et qui correspond à l’URI d’un fichier.

{"Container":"azureml-blobstore-bfe051f9-e8f5-424e-a59f-174a92b7aa97","SasToken":null,"Uri":"wasbs://azureml-blobstore-bfe051f9-e8f5-424e-a59f-174a92b7aa97@azmlfromscratchstr.blob.core.windows.net/azureml/a79bd916-2790-4202-89ed-fbdede3ccf1d/Trained_model/","Account":"azmlfromscratchstr","RelativePath":"azureml/a79bd916-2790-4202-89ed-fbdede3ccf1d/Trained_model/","PathType":0,"AmlDataStoreName":"workspaceblobstore"}

Nous allons voir à partir de l’explorateur ce que contient ce chemin.

Ici se trouve un fichier plus lourd que les autres : data.ilearner

Il s’agit du format (propriétaire ?) utilisé par Microsoft depuis Azure Machine Learning Studio, premier du nom, comme en témoigne cette documentation officielle.

Pour comprendre comment est utilisé ce fichier, nous ouvrons le fichier de scoring dans Visual Studio Code.

De nombreuses librairies apparaissent soulignées, elles ne sont pas disponibles et il s’agit d’éléments propres à l’espace de travail Azure Machine Learning. Le fichier iLearner sera chargé dans une classe ScoreModelModule dont la méthode .run() permettra d’obtenir une prévision.

Le modèle généré n’apparaît pas pour l’instant dans le menu Models de l’espace de travail.

Inférence par le Designer

Nous lançons maintenant un pipeline d’inférence, de type temps réel.

Le déploiement permet avoir de choisir entre une ressource Azure Container Instance (ACI) ou Kubernetes Service (AKS) pour porter un service web d’inférence.

C’est alors que le modèle est enregistré en tant qu’objet dans l’espace de travail Azure Machine Learning.

Nous remarquons le format (framework) CUSTOM qui n’est donc pas un des formats “classiques” commet SkLearn, ONNX ou encore PyTorch (liste complète ici).

Notons que le déploiement d’un point de terminaison génère une image dans le Container Registry lié au service Azure Machine Learning.

Peut-on restaurer les fichiers d’un pipeline du Designer ?

Nous en venons maintenant à la question cruciale du backup / restore des pipelines créés au travers du Designer.

Certain.e.s se souviendront du mode export dont nous disposions lorsque le Designer s’appelait Azure Machine Learning Studio, qui permettait de sauvegarder le pipeline dans un format JSON, pouvant ensuite être réimporté. Cette fonctionnalité ne semble aujourd’hui plus disponible mais nous avons vu que de nombreux fichiers sur le compte de stockage lié traduisent les manipulations réalisées dans l’interface visuelle.

Un test simple consistant à copier les répertoires vus ci-dessus dans un nouvel espace de travail Azure Machine Learning n’est pas concluant : rien n’apparait dans le nouvel espace. Ce n’est guère étonnant car les chemins semblent très liés au blob container d’origine (voir l’extrait de fichier Trained_model ).

Ma conclusion personnelle est donc qu’il n’est pas possible de restaurer les travaux réalisés avec le Designer dans une autre ressource Azure Machine Learning.

Faisons maintenant un dernier test : la suppression des fichiers du blob container correspondant au datastore. En relançant la création du point de terminaison, nous obtenons une erreur car le fichier binaire du modèle n’est pas accessible.

De ces tests, nous retenons quelques bonnes pratiques visant à sécuriser les développements réalisés dans l’interface graphique d’Azure Machine Learning :

  • créer un lock sur le groupe de ressources contenant Azure Machine Learning et le compte de stockage lié
  • utiliser un niveau de réplication a minima ZRS (ne pas choisir LRS qui est le choix par défaut)
  • mettre en place un système de réplication des fichiers du compte de stockage (object replication)
  • créer une documentation des pipelines réalisés (quelques copies d’écran et l’indication des paramètres qui ont été modifiés…)

Utiliser les commandes Git avec les instances de calcul Azure ML

Les instances de calcul (“compute instance“) d’Azure Machine Learning sont très pratiques pour les Data Scientists souhaitant travailler dans un environnement Jupyter, déjà configuré, et disposant du SDK azureml.

Cette simplicité ne doit pas faire oublier la bonne pratique de la gestion de versions du code, au moyen par exemple d’un outil comme Git.

Il existe des extensions pour JupyterLab qui permettent de faciliter les interactions avec un repository git. La recherche du mot-clé “git” nous en montre quelques-unes dans l’image ci-dessous.

Malheureusement, à ce jour (janvier 2022), des incompatibilités de versions semblent rendre impossible l’installation de jupyterlab-git, qui nécessite en particulier une version de JupyterLab >= 3.0.

L’installation va donc échouer.

Nous devrions utiliser la version de l’extension définie pour JupyterLab 2.x disponible dans cette branche : https://github.com/jupyterlab/jupyterlab-git/tree/jlab-2, à l’aide des commandes ci-dessous :

pip install git+https://github.com/jupyterlab/jupyterlab-git.git@jlab-2
conda install -c conda-forge jupyterlab-git

Après lancement d’un “Rebuild”, nous obtenons une erreur 500.

*

Nous allons donc nous en remettre aux lignes de commandes Git. Pour cela, il sera nécessaire d’ouvrir un terminal sur l’instance de calcul. Nous vérifions tout d’abord la bonne installation de git ainsi que sa version.

git --version
Git en version 2.33.0 est disponible

Nous allons créer ici un répertoire local à l’instance de calcul, que nous connecterons ensuite à un repository à distance, hébergé par Azure Repos. Ce répertoire contient un premier notebook, fichier de format .ipynb.

mkdir my_git_repo
git init

Il sera possible de renommer la branche master en main, selon les standards actuels, par la commande :

git branch -M main

Nous allons maintenant définir le repository à distance dont nous pourrons obtenir l’URL dans l’interface Azure DevOps, en cliquant sur le bouton “Clone”.

git remote add origin <Azure Repos URL>

Il ne reste plus qu’à lancer le trio de commandes qui permettront successivement d’ajouter les fichiers du dossier à la liste de ce qui doit être versionné, de “commiter” cette version avec un commentaire puis de pousser les fichiers vers le répertoire à distance.

git add .
git commit -m "initialize repository"
git push -u origin --all

Lors de la commande git push, il sera demandé à l’utilisateur de s’authentifier. Pour cela, nous pouvons obtenir un password dans la fenêtre vue précédemment : “Clone Repository”.

Les logs de la commande nous montrent que les fichiers sont bien uploadés sur le repository à distance.

Nous vérifions également la présence des fichiers dans l’interface graphique d’Azure Repos.

Rappelons enfin que les notebooks sont très verbeux lorsqu’on les regarde sous l’angle de fichiers texte que le gestionnaire de versions cherchera à comparer. Mieux vaut limiter le nombre de cellules et privilégier l’appel à des librairies externes, par exemple packagées dans un fichier wheel qui sera mis à disposition par Azure Feed (voir cet article).

Verbosité des notebooks pour un gestionnaire de versions

Se connecter à une instance de calcul Azure ML depuis Visual Studio Code

Dans un précédent article, nous explorions la procédure pour lancer simplement du code sur des machines distantes Azure ML (compute target) depuis l’environnement de développement (IDE) Visual Studio Code.

Dans les raccourcis disponibles au niveau du menu des instances de calcul, nous disposons dorénavant d’un lien permettant de lancer l’ouverture de Visual Studio Code.

Quel est l’avantage de travailler de la sorte ? Réponse : vous disposez de toute la puissance de l’IDE de Microsoft (en particulier pour les commandes Git), sans toutefois être dépendant de votre poste local.

Vous ne pouvez bien sûr utiliser qu’une instance qui vous est personnellement assignée.

Cliquez sur le lien VS Code et votre navigateur lèvera tout d’abord une alerte. Cochez la case “Toujours autoriser…” pour ne plus voir cette pop-up.

C’est maintenant au tour de VSC de faire une demande d’autorisation avant de lancer la connexion à distance.

En tâche de fond, nous assistons à l’installation d’un VS Code server sur l’instance de calcul.

Il ne reste plus qu’à “faire confiance” aux fichiers qui seront maintenant accessible depuis l’éditeur local.

Plusieurs alertes peuvent être levées par VS Code selon votre paramétrage. Ainsi, il faudra choisir un interpréteur Python (mais attention, ce n’est pas une distribution installée sur votre poste local !) ainsi que s’authentifier vis à vis de l’extension Azure pour VS Code (à installer également au préalable).

Si le choix entre Python2 et Python3 ne fait plus débat, le choix de la version mineure de Python est important pour votre projet, car celle-ci conditionne les versions de packages nécessaires par la suite.

Le réflexe pourrait alors être de choisir l’interpréteur Python du poste local mais nous voulons justement exécuter le code à distance. Une sélection dans la fenêtre ci-dessous n’est donc pas nécessaire.

Etape à ne PAS réaliser

Allons plutôt voir dans le coin en bas à droite de notre éditeur.

Un clic sur “Jupyter Server” va lancer un autre menu : “Pick how to connect to Jupyter“.

Nous choisissons ici “Azure ML Compute Instances” puis naviguons jusqu’à l’instance voulue. Un message d’alerte concernant la facturation de cette VM va alors apparaître.

Un rechargement de VS Code pourra être nécessaire pour bien valider la connexion.

Le statut en bas à droite de l’IDE nous indique que le serveur Jupyter se trouve bien à distance (remote) et c’est donc lui qui exécutera le code.

Commençons par un commande simple dans un nouveau notebook.

Il est maintenant temps de choisir l’interpréteur Python de la machine distante (cliquer si besoin sur le bouton “sélectionner le noyau”).

Nous choisissons ici l’environnement Python 3.8, muni des packages azureml, qui nous permettront d’interagir avec le service Azure Machine Learning.

Vérification de la version du package azureml-core

Utiliser les commandes Git de VSC

C’est sans doute le plus grand des avantages à passer par l’IDE : pouvoir réaliser du contrôle de code source, avec les fonctionnalités Git intégrées dans VSC.

En cliquant sur le symbole Git du menu du gauche, nous déroulons une première fenêtre demandant de réaliser l’initialisation du repository.

Il est ensuite nécessaire de configurer le nom de l’utilisation ainsi que son adresse de courrier électronique.

Les changements (ici, tous les fichiers lors de l’initialisation) sont détectés par Git et il sera possible de les pousser (git push) sur le repository.

En cliquant ici sur Push, une alerte sera levée car une dernière configuration est nécessaire.

Il faut configurer l’URL du repository, par exemple une adresse GitHub.

Ceci se réalise au moyen de la commande ci-dessous, sur la branche principale (main).

git push --set-upstream <origin_branch> main

Ca y est ! Nous disposons maintenant d’une configuration très efficace pour travailler nos développements de Machine Learning, en lien avec le service Azure ML. Ce système permet également une véritable isolation des fichiers pour chaque développeur. Aucun risque qu’un.e collègue vienne modifier vos scripts, autrement qu’au travers d’un merge de branches.