Distribuire modelli MLflow in distribuzioni batch

SI APPLICA A:Estensione ML dell'interfaccia della riga di comando di Azure v2 (corrente)Python SDK azure-ai-ml v2 (corrente)

Questo articolo illustra come distribuire modelli MLflow in Azure Machine Learning per l'inferenza batch usando endpoint batch. Quando si distribuiscono modelli MLflow in endpoint batch, Azure Machine Learning:

  • Fornisce un ambiente di base di MLflow image/curate che contiene le dipendenze necessarie per eseguire un processo batch di Azure Machine Learning.
  • Crea una pipeline di processi batch con uno script di assegnazione dei punteggi che è possibile usare per elaborare i dati usando la parallelizzazione.

Nota

Per altre informazioni sui tipi di file di input supportati e sui dettagli sul funzionamento del modello MLflow, vedere Considerazioni durante la distribuzione nell'inferenza batch.

Informazioni su questo esempio

Questo esempio illustra come distribuire un modello MLflow in un endpoint batch per eseguire stime batch. Questo esempio usa un modello MLflow basato sul set di dati UCI Heart Disease. Il database contiene 76 attributi, ma viene usato un subset di 14 attributi. Il modello cerca di prevedere la presenza di una malattia cardiaca in un paziente. Il valore è intero compreso tra 0 (nessuna presenza) e 1 (presenza).

Il training del modello è stato eseguito tramite un classificatore XGBBoost e tutta la preelaborazione necessaria è stata inserita in un pacchetto come pipeline scikit-learn, rendendo questo modello una pipeline end-to-end che va dai dati non elaborati alle previsioni.

L'esempio contenuto in questo articolo si basa sugli esempi di codice contenuti nel repository azureml-examples. Per eseguire i comandi in locale senza dover copiare o incollare il file YAML e altri file, innanzitutto clonare il repository quindi cambiare le directory nella cartella:

git clone https://github.com/Azure/azureml-examples --depth 1
cd azureml-examples/cli

I file per questo esempio si trovano in:

cd endpoints/batch/deploy-models/heart-classifier-mlflow

Seguire la procedura in Jupyter Notebooks

È possibile seguire questo esempio nei notebook seguenti. Nel repository clonato aprire il notebook: mlflow-for-batch-tabular.ipynb.

Prerequisiti

Prima di seguire la procedura descritta in questo articolo, assicurarsi di disporre dei prerequisiti seguenti:

  • Una sottoscrizione di Azure. Se non si ha una sottoscrizione di Azure, creare un account gratuito prima di iniziare. Provare la versione gratuita o a pagamento di Azure Machine Learning.

  • Un'area di lavoro di Azure Machine Learning. Se non è disponibile, usare la procedura descritta nell'articolo Gestire le aree di lavoro di Azure Machine Learning per crearne una.

  • Assicurarsi di disporre delle autorizzazioni seguenti nell'area di lavoro:

    • Creare o gestire endpoint e distribuzioni batch: usare un ruolo Proprietario, Collaboratore o Personalizzato che consenta Microsoft.MachineLearningServices/workspaces/batchEndpoints/*.

    • Creare distribuzioni arm nel gruppo di risorse dell'area di lavoro: usare un ruolo Proprietario, Collaboratore o Personalizzato che consenta Microsoft.Resources/deployments/write nel gruppo di risorse in cui viene distribuita l'area di lavoro.

  • Per usare Azure Machine Learning, è necessario installare il software seguente:

    L'Interfaccia della riga di comando di Azure e l'mlestensione per Azure Machine Learning.

    az extension add -n ml
    

    Nota

    Le distribuzioni dei componenti della pipeline per gli endpoint batch sono state introdotte nella versione 2.7 dell'estensione ml per l'interfaccia della riga di comando di Azure. Usare az extension update --name ml per ottenere l'ultima versione.

Connettersi all'area di lavoro

L'area di lavoro è la risorsa di primo livello per Azure Machine Learning, che fornisce una posizione centralizzata da cui gestire tutti gli artefatti creati quando si usa Azure Machine Learning. In questa sezione ci si connetterà all'area di lavoro in cui verranno eseguite le attività di distribuzione.

Passare i valori per l'ID sottoscrizione, l'area di lavoro, la posizione e il gruppo di risorse nel codice seguente:

az account set --subscription <subscription>
az configure --defaults workspace=<workspace> group=<resource-group> location=<location>

Passaggi

Seguire questa procedura per distribuire un modello MLflow in un endpoint batch per l'esecuzione dell'inferenza batch sui nuovi dati:

  1. L'endpoint batch può distribuire solo i modelli registrati. In questo caso, è già presente una copia locale del modello nel repository, quindi è sufficiente pubblicare il modello nel registro dell'area di lavoro. È possibile ignorare questo passaggio se il modello che si sta tentando di distribuire è già registrato.

    MODEL_NAME='heart-classifier-mlflow'
    az ml model create --name $MODEL_NAME --type "mlflow_model" --path "model"
    
  2. Prima di procedere, è necessario assicurarsi che le distribuzioni batch che verranno create possano essere eseguite in un'infrastruttura (calcolo). Le distribuzioni batch possono essere eseguite in qualsiasi ambiente di calcolo di Azure Machine Learning già esistente nell'area di lavoro. Ciò significa che più distribuzioni batch possono condividere la stessa infrastruttura di calcolo. In questo esempio si lavorerà su un cluster di calcolo di Azure Machine Learning denominato cpu-cluster. Verificare che il calcolo esista nell'area di lavoro o crearlo in caso contrario.

    Creare un cluster di calcolo come segue:

    az ml compute create -n batch-cluster --type amlcompute --min-instances 0 --max-instances 5
    
  3. A questo punto è possibile creare l'endpoint e la distribuzione batch. Iniziamo prima con l'endpoint. Gli endpoint richiedono solo un nome e una descrizione da creare. Il nome dell'endpoint verrà incluso nell'URI associato all'endpoint. Per questo motivo, i nomi degli endpoint batch devono essere univoci all'interno di un'area di Azure. Ad esempio, può essere presente un solo endpoint batch con il nome mybatchendpoint in westus2.

    In questo caso, inserire il nome dell'endpoint in una variabile in modo da potervi fare facilmente riferimento in un secondo momento.

    ENDPOINT_NAME="heart-classifier"
    
  4. Creare l'endpoint:

    Per creare un nuovo endpoint, creare una YAML configurazione simile alla seguente:

    endpoint.yml

    $schema: https://azuremlschemas.azureedge.net/latest/batchEndpoint.schema.json
    name: heart-classifier-batch
    description: A heart condition classifier for batch inference
    auth_mode: aad_token
    

    Creare quindi l'endpoint con il comando seguente:

    az ml batch-endpoint create -n $ENDPOINT_NAME -f endpoint.yml
    
  5. A questo punto, è possibile creare la distribuzione. I modelli MLflow non richiedono di indicare un ambiente o uno script di assegnazione dei punteggi durante la creazione delle distribuzioni poiché vengono create automaticamente. Tuttavia, è possibile specificarli se si vuole personalizzare il modo in cui la distribuzione esegue l'inferenza.

    Per creare una nuova distribuzione nell'endpoint creato, creare una YAML configurazione simile alla seguente. È possibile controllare lo schema YAML dell'endpoint batch completo per ottenere proprietà aggiuntive.

    deployment-simple/deployment.yml

    $schema: https://azuremlschemas.azureedge.net/latest/modelBatchDeployment.schema.json
    endpoint_name: heart-classifier-batch
    name: classifier-xgboost-mlflow
    description: A heart condition classifier based on XGBoost
    type: model
    model: azureml:heart-classifier-mlflow@latest
    compute: azureml:batch-cluster
    resources:
      instance_count: 2
    settings:
      max_concurrency_per_instance: 2
      mini_batch_size: 2
      output_action: append_row
      output_file_name: predictions.csv
      retry_settings:
        max_retries: 3
        timeout: 300
      error_threshold: -1
      logging_level: info
    

    Creare quindi la distribuzione con il comando seguente:

    az ml batch-deployment create --file deployment-simple/deployment.yml --endpoint-name $ENDPOINT_NAME --set-default
    

    Importante

    Configurare timeout nella distribuzione in base al tempo necessario per l'esecuzione dell'inferenza del modello in un singolo batch. Maggiore è la dimensione del batch maggiore deve essere questo valore. Remeber che mini_batch_size indica il numero di file in un batch, non il numero di campioni. Quando si usano dati tabulari, ogni file può contenere più righe che aumentano il tempo necessario per l'elaborazione di ogni file da parte dell'endpoint batch. Usare valori elevati in questi casi per evitare errori di timeout.

  6. Sebbene sia possibile richiamare una distribuzione specifica all'interno di un endpoint, in genere si vuole richiamare l'endpoint stesso e consentire all'endpoint di decidere quale distribuzione usare. Tale distribuzione è denominata distribuzione "predefinita". In questo modo è possibile modificare la distribuzione predefinita e quindi modificare il modello che gestisce la distribuzione senza modificare il contratto con l'utente che richiama l'endpoint. Usare l'istruzione seguente per aggiornare la distribuzione predefinita:

    DEPLOYMENT_NAME="classifier-xgboost-mlflow"
    az ml batch-endpoint update --name $ENDPOINT_NAME --set defaults.deployment_name=$DEPLOYMENT_NAME
    
  7. A questo punto, l'endpoint batch è pronto per essere usato.

Test della distribuzione

Per testare l'endpoint, si userà un esempio di dati senza etichetta che si trovano in questo repository e che possono essere usati con il modello. Gli endpoint batch possono elaborare solo i dati che si trovano nel cloud e sono accessibili dall'area di lavoro di Azure Machine Learning. In questo esempio verrà caricato in un archivio dati di Azure Machine Learning. In particolare, verrà creato un asset di dati che può essere usato per richiamare l'endpoint per l'assegnazione dei punteggi. Si noti tuttavia che gli endpoint batch accettano dati che possono essere inseriti in diverse posizioni.

  1. Creare prima l'asset di dati. Questo asset di dati è costituito da una cartella con più file CSV da elaborare in parallelo usando endpoint batch. È possibile ignorare questo passaggio perché i dati sono già registrati come asset di dati o si vuole usare un tipo di input diverso.

    a. Creare una definizione di asset di dati in YAML:

    heart-dataset-unlabeled.yml

    $schema: https://azuremlschemas.azureedge.net/latest/data.schema.json
    name: heart-dataset-unlabeled
    description: An unlabeled dataset for heart classification.
    type: uri_folder
    path: data
    

    b. Creare l'asset di dati:

    az ml data create -f heart-dataset-unlabeled.yml
    
  2. Ora che i dati sono stati caricati e pronti per l'uso, richiamare l'endpoint:

    JOB_NAME = $(az ml batch-endpoint invoke --name $ENDPOINT_NAME --input azureml:heart-dataset-unlabeled@latest --query name -o tsv)
    

    Nota

    L'utilità jq potrebbe non essere installata in ogni installazione. È possibile ottenere le istruzioni di installazione in questo collegamento.

    Suggerimento

    Si noti che non si indica il nome della distribuzione nell'operazione invoke. Questo perché l'endpoint instrada automaticamente il processo alla distribuzione predefinita. Poiché l'endpoint ha una sola distribuzione, quella predefinita è quella. È possibile specificare come destinazione una distribuzione specifica indicando l'argomento/parametro deployment_name.

  3. Un processo batch viene avviato non appena viene restituito il comando. È possibile monitorare lo stato del processo fino al termine dell'operazione:

    az ml job show -n $JOB_NAME --web
    

Analisi degli output

Le stime di output vengono generate nel predictions.csv file come indicato nella configurazione della distribuzione. Il processo genera un output denominato denominato score in cui viene inserito il file. Viene generato un solo file per ogni processo batch.

Il file è strutturato come segue:

  • È presente una riga per ogni punto dati inviato al modello. Per i dati tabulari, significa che il file (predictions.csv) contiene una riga per ogni riga presente in ognuno dei file elaborati. Per altri tipi di dati (ad esempio immagini, audio, testo), è presente una riga per ogni file elaborato.

  • Le colonne seguenti si trovano nel file (in ordine):

    • row (facoltativo), indice di riga corrispondente nel file di dati di input. Questo vale solo se i dati di input sono tabulari. Le stime vengono restituite nello stesso ordine in cui vengono visualizzate nel file di input, in modo da poter fare affidamento sul numero di riga in modo che corrisponda alla stima corrispondente.
    • prediction, la stima associata ai dati di input. Questo valore viene restituito "così come è" fornito dalla funzione del predict(). modello.
    • file_name, il nome del file da cui sono stati letti i dati. Nei dati tabulari usare questo campo per sapere quale stima appartiene ai dati di input.

È possibile scaricare i risultati del processo usando il nome del processo:

Per scaricare le previsioni, usare il comando seguente:

az ml job download --name $JOB_NAME --output-name score --download-path ./

Una volta scaricato il file, è possibile aprirlo usando lo strumento preferito. Nell'esempio seguente vengono caricate le previsioni usando il dataframe Pandas.

import pandas as pd

score = pd.read_csv(
    "named-outputs/score/predictions.csv", names=["row", "prediction", "file"]
)

L'output sarà simile al seguente:

riga stima file
0 0 heart-unlabeled-0.csv
1 1 heart-unlabeled-0.csv
2 0 heart-unlabeled-0.csv
... ... ...
307 0 heart-unlabeled-3.csv

Suggerimento

Si noti che in questo esempio i dati di input sono dati tabulari in CSV formato e sono presenti 4 file di input diversi (heart-unlabeled-0.csv, heart-unlabeled-1.csv, heart-unlabeled-2.csv e heart-unlabeled-3.csv).

Considerazioni sulla distribuzione nell'inferenza batch

Azure Machine Learning supporta la distribuzione di modelli MLflow in endpoint batch senza indicare uno script di assegnazione dei punteggi. Rappresenta un modo pratico per distribuire modelli che richiedono l'elaborazione di grandi quantità di dati in modo batch. Azure Machine Learning usa le informazioni nella specifica del modello MLflow per orchestrare il processo di inferenza.

Come viene distribuito il lavoro sui ruoli di lavoro

Gli endpoint batch distribuiscono il lavoro a livello di file, sia per i dati strutturati che non strutturati. Di conseguenza, per questa funzionalità sono supportati solo i file URI e le cartelle URI. Ogni ruolo di lavoro elabora batch di Mini batch size file alla volta. Per i dati tabulari, gli endpoint batch non prendono in considerazione il numero di righe all'interno di ogni file durante la distribuzione del lavoro.

Avviso

Le strutture di cartelle annidate non vengono esaminate durante l'inferenza. Se si partiziona i dati usando cartelle, assicurarsi di rendere flat la struttura in anticipo.

Le distribuzioni batch chiameranno la predict funzione del modello MLflow una volta per ogni file. Per i file CSV contenenti più righe, questo può imporre una pressione di memoria nel calcolo sottostante e può aumentare il tempo necessario per il modello per assegnare un punteggio a un singolo file (in particolare per modelli di linguaggio di grandi dimensioni). Se si verificano diverse eccezioni di memoria insufficiente o voci di timeout nei log, è consigliabile suddividere i dati in file più piccoli con meno righe o implementare l'invio in batch a livello di riga all'interno dello script di modello/assegnazione dei punteggi.

Supporto dei tipi di file

I tipi di dati seguenti sono supportati per l'inferenza batch durante la distribuzione di modelli MLflow senza un ambiente e uno script di assegnazione dei punteggi. Se si vuole elaborare un tipo di file diverso o eseguire l'inferenza in modo diverso che gli endpoint batch eseguono per impostazione predefinita, è sempre possibile creare la distribuzione con uno script di assegnazione dei punteggi, come illustrato in Uso di modelli MLflow con uno script di assegnazione dei punteggi.

Estensione di file Tipo restituito come input del modello Requisito di firma
.csv, .parquet, .pqt pd.DataFrame ColSpec. Se non specificato, la digitazione delle colonne non viene applicata.
.png, .jpg, .jpeg, .tiff, .bmp.gif np.ndarray TensorSpec. L'input viene rimodellato in modo che corrisponda alla forma tensori, se disponibile. Se non è disponibile alcuna firma, vengono dedotti tensori di tipo np.uint8. Per altre indicazioni, vedere Considerazioni per i modelli MLflow che elaborano immagini.

Avviso

Si noti che qualsiasi file non supportato che potrebbe essere presente nei dati di input comporterà l'esito negativo del processo. Verrà visualizzata una voce di errore come indicato di seguito: "ERROR:azureml:Error processing input file: '/mnt/batch/tasks/.../a-given-file.avro'. Il tipo di file 'avro' non è supportato.".

Imposizione della firma per i modelli MLflow

I tipi di dati di input vengono applicati dai processi di distribuzione batch durante la lettura dei dati usando la firma del modello MLflow disponibile. Ciò significa che l'input dei dati deve essere conforme ai tipi indicati nella firma del modello. Se i dati non possono essere analizzati come previsto, il processo avrà esito negativo con un messaggio di errore simile al seguente: "ERROR:azureml:Error processing input file: '/mnt/batch/tasks/.../a-given-file.csv'. Eccezione: valore letterale non valido per int() con base 10: 'value'"..

Suggerimento

Le firme nei modelli MLflow sono facoltative, ma sono altamente incoraggiate perché offrono un modo pratico per rilevare in anticipo i problemi di compatibilità dei dati. Per altre informazioni su come registrare i modelli con firme, vedere Registrazione di modelli con una firma personalizzata, un ambiente o esempi.

È possibile esaminare la firma del modello aprendo il MLmodel file associato al modello MLflow. Per altre informazioni sul funzionamento delle firme in MLflow, vedere Firme in MLflow.

Supporto di Flavor

Le distribuzioni batch supportano solo la distribuzione di modelli MLflow con un pyfunc sapore. Se è necessario distribuire un tipo diverso, vedere Uso di modelli MLflow con uno script di assegnazione dei punteggi.

Personalizzazione delle distribuzioni di modelli MLflow con uno script di assegnazione dei punteggi

I modelli MLflow possono essere distribuiti negli endpoint batch senza indicare uno script di assegnazione dei punteggi nella definizione di distribuzione. Tuttavia, è possibile acconsentire esplicitamente a indicare questo file (in genere definito driver batch) per personalizzare la modalità di esecuzione dell'inferenza.

In genere si seleziona questo flusso di lavoro quando:

  • È necessario elaborare un tipo di file non supportato dalle distribuzioni batch MLflow.
  • È necessario personalizzare la modalità di esecuzione del modello, ad esempio usare un sapore specifico per caricarlo con mlflow.<flavor>.load().
  • È necessario eseguire l'elaborazione di pre/pos nella routine di assegnazione dei punteggi quando non viene eseguita dal modello stesso.
  • L'output del modello non può essere rappresentato correttamente nei dati tabulari. Ad esempio, è un tensore che rappresenta un'immagine.
  • Il modello non può elaborare ogni file contemporaneamente a causa di vincoli di memoria e deve leggerlo in blocchi.

Importante

Se si sceglie di indicare uno script di assegnazione dei punteggi per una distribuzione del modello MLflow, sarà necessario specificare anche l'ambiente in cui verrà eseguita la distribuzione.

Passaggi

Usare la procedura seguente per distribuire un modello MLflow con uno script di assegnazione dei punteggi personalizzato.

  1. Identificare la cartella in cui è posizionato il modello MLflow.

    a. Passare al portale di Azure Machine Learning.

    b. Passare alla sezione Modelli.

    c. Selezionare il modello che si sta tentando di distribuire e fare clic sulla scheda Artefatti.

    d. Prendere nota della cartella visualizzata. Questa cartella è stata indicata quando il modello è stato registrato.

    Screenshot che mostra la cartella in cui sono posizionati gli artefatti del modello.

  2. Creare uno script di assegnazione del punteggio. Si noti che il nome model della cartella identificato prima è stato incluso nella init() funzione.

    deployment-custom/code/batch_driver.py

    # Copyright (c) Microsoft. All rights reserved.
    # Licensed under the MIT license.
    
    import os
    import glob
    import mlflow
    import pandas as pd
    import logging
    
    
    def init():
        global model
        global model_input_types
        global model_output_names
    
        # AZUREML_MODEL_DIR is an environment variable created during deployment
        # It is the path to the model folder
        # Please provide your model's folder name if there's one
        model_path = glob.glob(os.environ["AZUREML_MODEL_DIR"] + "/*/")[0]
    
        # Load the model, it's input types and output names
        model = mlflow.pyfunc.load(model_path)
        if model.metadata and model.metadata.signature:
            if model.metadata.signature.inputs:
                model_input_types = dict(
                    zip(
                        model.metadata.signature.inputs.input_names(),
                        model.metadata.signature.inputs.pandas_types(),
                    )
                )
            if model.metadata.signature.outputs:
                if model.metadata.signature.outputs.has_input_names():
                    model_output_names = model.metadata.signature.outputs.input_names()
                elif len(model.metadata.signature.outputs.input_names()) == 1:
                    model_output_names = ["prediction"]
        else:
            logging.warning(
                "Model doesn't contain a signature. Input data types won't be enforced."
            )
    
    
    def run(mini_batch):
        print(f"run method start: {__file__}, run({len(mini_batch)} files)")
    
        data = pd.concat(
            map(
                lambda fp: pd.read_csv(fp).assign(filename=os.path.basename(fp)), mini_batch
            )
        )
    
        if model_input_types:
            data = data.astype(model_input_types)
    
        # Predict over the input data, minus the column filename which is not part of the model.
        pred = model.predict(data.drop("filename", axis=1))
    
        if pred is not pd.DataFrame:
            if not model_output_names:
                model_output_names = ["pred_col" + str(i) for i in range(pred.shape[1])]
            pred = pd.DataFrame(pred, columns=model_output_names)
    
        return pd.concat([data, pred], axis=1)
    
  3. Verrà ora creato un ambiente in cui è possibile eseguire lo script di assegnazione dei punteggi. Poiché il modello è MLflow, i requisiti di conda vengono specificati anche nel pacchetto del modello (per altri dettagli sui modelli MLflow e sui file inclusi in esso, vedere Il formato MLmodel). Si creerà quindi l'ambiente usando le dipendenze conda dal file. È tuttavia necessario includere anche il pacchetto azureml-core necessario per le distribuzioni batch.

    Suggerimento

    Se il modello è già registrato nel Registro di sistema dei modelli, è possibile scaricare/copiare il conda.yml file associato al modello passando a studio di Azure Machine Learning> Modelli > Selezionare il modello dall'elenco > Artefatti. Aprire la cartella radice nel riquadro di spostamento e selezionare il conda.yml file elencato. Fare clic su Scarica o copiarne il contenuto.

    Importante

    In questo esempio viene usato un ambiente conda specificato in /heart-classifier-mlflow/environment/conda.yaml. Questo file è stato creato combinando il file di dipendenze MLflow conda originale e aggiungendo il pacchetto azureml-core. Non è possibile usare direttamente il conda.yml file dal modello.

    La definizione dell'ambiente verrà inclusa nella definizione di distribuzione stessa come ambiente anonimo. Nella distribuzione verranno visualizzate le righe seguenti:

    environment:
      name: batch-mlflow-xgboost
      image: mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04:latest
      conda_file: environment/conda.yaml
    
  4. Configurare la distribuzione:

    Per creare una nuova distribuzione nell'endpoint creato, creare una YAML configurazione simile alla seguente. È possibile controllare lo schema YAML dell'endpoint batch completo per ottenere proprietà aggiuntive.

    distribuzione personalizzata/deployment.yml

    $schema: https://azuremlschemas.azureedge.net/latest/modelBatchDeployment.schema.json
    endpoint_name: heart-classifier-batch
    name: classifier-xgboost-custom
    description: A heart condition classifier based on XGBoost
    type: model
    model: azureml:heart-classifier-mlflow@latest
    environment:
      name: batch-mlflow-xgboost
      image: mcr.microsoft.com/azureml/openmpi4.1.0-ubuntu20.04:latest
      conda_file: environment/conda.yaml
    code_configuration:
      code: code
      scoring_script: batch_driver.py
    compute: azureml:batch-cluster
    resources:
      instance_count: 2
    settings:
      max_concurrency_per_instance: 2
      mini_batch_size: 2
      output_action: append_row
      output_file_name: predictions.csv
      retry_settings:
        max_retries: 3
        timeout: 300
      error_threshold: -1
      logging_level: info
    
  5. Creare ora la distribuzione:

    az ml batch-deployment create --file deployment-custom/deployment.yml --endpoint-name $ENDPOINT_NAME
    
  6. A questo punto, l'endpoint batch è pronto per essere usato.

Pulire le risorse

Eseguire il codice seguente per eliminare l'endpoint batch e tutte le distribuzioni sottostanti. I processi di assegnazione dei punteggi batch non verranno eliminati.

az ml batch-endpoint delete --name $ENDPOINT_NAME --yes

Passaggi successivi