Il problema vero non è addestrare un modello: è sapere, sei settimane dopo, quale delle quaranta varianti provate ha generato il modello in produzione, con quali iperparametri, su quale versione dei dati e con quale commit. Un MLOps engineer non è misurato sulla metrica migliore ottenuta una volta, ma sulla capacità di riprodurla, e su Azure Machine Learning l’infrastruttura di quella riproduzione è MLflow.

Il workspace è già un tracking server MLflow

Il workspace di Azure Machine Learning è MLflow-compatible: espone direttamente le API di MLflow e si comporta da tracking server, quindi Microsoft non ospita istanze di MLflow server per conto tuo e tu non devi installarne una. Il modo canonico di loggare qui è dunque la libreria open source MLflow, non un SDK proprietario: l’SDK Python v2 azure-ai-ml non contiene funzioni di logging, differenza esplicita rispetto alla v1, e lo stesso vale per rileggere le metriche. Sul client servono due pacchetti, pip install mlflow azureml-mlflow: il plugin è ciò che insegna a MLflow a parlare con il workspace.

Dove gira il codice cambia la configurazione, e la distinzione è materiale d’esame. Dentro un job di Azure Machine Learning il tracking URI è già impostato e il run è già aperto: non serve chiamare mlflow.start_run(), basta importare mlflow e loggare. Fuori — sul portatile, su un runner GitHub Actions, su un compute di terze parti — devi puntare MLflow al tracking URI del workspace e autenticarti.

Anche il nome dell’esperimento segue la stessa logica: in interattivo lo imposti con mlflow.set_experiment("..."), in un job CLI v2 lo dichiari nel YAML con experiment_name e lo script resta invariato. È la regola del modello v2: la configurazione sta nel file versionato, non nel codice di addestramento.

Cosa MLflow registra da solo e cosa devi registrare tu

mlflow.autolog(), messo prima del codice di addestramento, attiva l’autologging: per molte librerie diffuse MLflow cattura parametri, metriche e artefatti senza che tu scriva altro. Due avvertenze ricorrono nelle domande. Cosa venga catturato lo decide il singolo framework, non Azure Machine Learning, quindi flavor diversi tracciano insiemi diversi di valori; e alcuni framework smettono di loggare il modello oltre certe soglie, per cui l’autologging non garantisce che l’artefatto esista. Puoi anche prenderne solo una parte: mlflow.autolog(log_models=False) mantiene metriche e parametri e lascia a te il modello.

Tutto ciò che è specifico del tuo problema lo registri tu: log_param e log_params per gli iperparametri, log_metric per i valori numerici, log_artifact e log_artifacts per file e cartelle, log_image e log_figure per i grafici, log_text e log_dict per note e configurazioni.

import mlflow

mlflow.autolog(log_models=False)

for epoch in range(epochs):
    loss = train_one_epoch()
    mlflow.log_metric("train_loss", float(loss), step=epoch)

mlflow.log_param("feature_set", "v3")
mlflow.sklearn.log_model(model, "model", signature=signature)

Una curva si ottiene loggando più volte la stessa metrica con step diverso, ma run.data.metrics e mlflow.search_runs() restituiscono solo l’ultimo valore di ciascuna metrica: per la serie completa serve MlflowClient.get_metric_history(). Se il job produce moltissimi valori, o gira su molti nodi che scrivono insieme, conviene il logging asincrono con mlflow.config.enable_async_logging() o con synchronous=False; l’ordine resta garantito.

Vale doppio la differenza fra artefatto e modello: mlflow.<flavor>.log_model() produce un modello MLflow con file MLmodel, flavor, signature e dipendenze, ed è quella struttura ad abilitare il deployment senza scoring script e la Responsible AI dashboard. Un pickle passato a log_artifact() non offre nulla di tutto questo.

Il notebook è un punto di partenza, non un artefatto di produzione

La compute instance è la workstation gestita del workspace: ha un solo proprietario, arriva con Jupyter, JupyterLab e VS Code integrati e con driver e framework preinstallati, e può fare anche da compute target di training, però a nodo singolo. I notebook non vivono sul disco della macchina: stanno nella file share Azure dello storage account del workspace, sotto «User files». Da qui tre conseguenze: sopravvivono allo stop e alla cancellazione dell’istanza, sono visibili a tutte le compute instance del workspace, e non sono il posto dei dati di addestramento, che stanno su datastore e data asset.

Il notebook serve a guardare i dati e a scartare ipotesi. Non è un artefatto di produzione per ragioni strutturali: non ha un contratto di input e output, il risultato dipende dall’ordine in cui hai eseguito le celle, e una pipeline di CI non lo esegue in modo affidabile. La promozione ha una forma sola: il codice esce dal notebook e diventa uno script, i valori magici diventano argomenti da riga di comando, la sottomissione diventa un command job con environment e dati dichiarati in YAML.

Un pezzo di tracciabilità arriva gratis: se sottometti un job da una cartella che è un repository Git locale, Azure Machine Learning registra repository, branch e commit come proprietà di sistema del job, nelle famiglie azureml.git.* e mlflow.source.git.*. E la compute instance si paga finché è accesa: idle shutdown e schedule di avvio e arresto sono configurazioni, non optional.

Da ricordare per l esame

  • Il workspace di Azure Machine Learning è esso stesso il tracking server MLflow; l’SDK Python v2 non ha API di logging, si logga con mlflow più il plugin azureml-mlflow.
  • Dentro un job il run è già avviato e il tracking URI già impostato; fuori da Azure Machine Learning il tracking URI del workspace lo configuri tu.
  • mlflow.autolog() cattura ciò che decide il framework: metriche di dominio, parametri custom e il modello con signature restano compito tuo.
  • search_runs e run.data.metrics danno solo l’ultimo valore di una metrica; per la serie completa serve get_metric_history().
  • I notebook stanno nella file share del workspace e si condividono fra compute instance; i dati di training no, e il codice che diventa serio va estratto in uno script eseguito come command job.