Deployare 🤗 ViT su Kubernetes con TF Serving
Deploy 🤗 ViT su Kubernetes con TF Serving
Nel post precedente, abbiamo mostrato come distribuire un modello Vision Transformer (ViT) di 🤗 Transformers localmente con TensorFlow Serving. Abbiamo coperto argomenti come l’elaborazione dell’embedding e le operazioni di preprocessing e postprocessing all’interno del modello Vision Transformer, la gestione delle richieste gRPC e altro ancora!
Mentre le distribuzioni locali sono un ottimo punto di partenza per costruire qualcosa di utile, è necessario eseguire distribuzioni che possano servire molti utenti in progetti reali. In questo post, imparerai come scalare la distribuzione locale del post precedente con Docker e Kubernetes. Pertanto, assumiamo una certa familiarità con Docker e Kubernetes.
Questo post si basa sul post precedente, quindi consigliamo vivamente di leggerlo prima. Puoi trovare tutto il codice discusso in questo repository.
Il flusso di lavoro di base per scalare una distribuzione come la nostra include i seguenti passaggi:
- Filosofia di TensorFlow di Hugging Face
- Presentazione di Skops
- Un’introduzione semplice alla moltiplicazione di matrici a 8 bit per i transformers su larga scala utilizzando transformers, accelerate e bitsandbytes
-
Containerizzare la logica dell’applicazione: La logica dell’applicazione coinvolge un modello servito che può gestire le richieste e restituire le previsioni. Per la containerizzazione, Docker è lo standard del settore.
-
Deployare il container Docker: Hai diverse opzioni qui. L’opzione più utilizzata è distribuire il container Docker su un cluster Kubernetes. Kubernetes fornisce numerose funzionalità di distribuzione (ad esempio, autoscaling e sicurezza). Puoi utilizzare una soluzione come Minikube per gestire i cluster Kubernetes localmente o una soluzione serverless come Elastic Kubernetes Service (EKS).
Potresti chiederti perché utilizzare una configurazione esplicita come questa nell’era di Sagemaker, Vertex AI, che fornisce funzionalità specifiche per la distribuzione di ML fin dall’inizio. È giusto pensarci.
Il flusso di lavoro sopra è ampiamente adottato nel settore e molte organizzazioni ne beneficiano. È già stato testato sul campo per molti anni. Ti consente inoltre di avere un controllo più granulare delle tue distribuzioni mentre astrae le parti non banali.
Questo post utilizza Google Kubernetes Engine (GKE) per fornire e gestire un cluster Kubernetes. Assumiamo che tu abbia già un progetto GCP abilitato alla fatturazione se stai usando GKE. Inoltre, tieni presente che dovresti configurare l’utilità gcloud
per eseguire il deployment su GKE. Ma i concetti discussi in questo post si applicano anche se decidi di utilizzare Minikube.
Nota: I frammenti di codice mostrati in questo post possono essere eseguiti su un terminale Unix purché tu abbia configurato l’utilità gcloud
insieme a Docker e kubectl
. Ulteriori istruzioni sono disponibili nel repository allegato.
Il modello di servizio può gestire input immagine grezzi come byte ed è in grado di elaborare il preprocessing e il postprocessing.
In questa sezione, vedrai come containerizzare quel modello utilizzando l’immagine di base TensorFlow Serving. TensorFlow Serving consuma modelli nel formato SavedModel
. Ricorda come hai ottenuto un tale SavedModel
nel post precedente. Assumiamo che tu abbia il SavedModel
compresso nel formato tar.gz
. Puoi scaricarlo da qui nel caso. Quindi il SavedModel
dovrebbe essere posizionato nella struttura di directory speciale di <MODEL_NAME>/<VERSION>/<SavedModel>
. In questo modo TensorFlow Serving gestisce contemporaneamente più distribuzioni di modelli versionati diversi.
Preparazione dell’immagine Docker
Lo script shell di seguito posiziona il SavedModel
in hf-vit/1
sotto la directory padre models. Copierai tutto ciò che è contenuto al suo interno durante la preparazione dell’immagine Docker. In questo esempio c’è solo un modello, ma questo è un approccio più generalizzabile.
$ MODEL_TAR=model.tar.gz
$ MODEL_NAME=hf-vit
$ MODEL_VERSION=1
$ MODEL_PATH=models/$MODEL_NAME/$MODEL_VERSION
$ mkdir -p $MODEL_PATH
$ tar -xvf $MODEL_TAR --directory $MODEL_PATH
Sotto, mostriamo come è strutturata la directory models
nel nostro caso:
$ find /models
/models
/models/hf-vit
/models/hf-vit/1
/models/hf-vit/1/keras_metadata.pb
/models/hf-vit/1/variables
/models/hf-vit/1/variables/variables.index
/models/hf-vit/1/variables/variables.data-00000-of-00001
/models/hf-vit/1/assets
/models/hf-vit/1/saved_model.pb
L’immagine personalizzata di TensorFlow Serving dovrebbe essere costruita sulla base di quella di base. Ci sono vari approcci per farlo, ma lo faremo eseguendo un container Docker come illustrato nel documento ufficiale. Iniziamo eseguendo l’immagine tensorflow/serving
in modalità di background, quindi l’intera directory models
viene copiata nel container in esecuzione come mostrato di seguito.
$ docker run -d --name serving_base tensorflow/serving
$ docker cp models/ serving_base:/models/
Abbiamo utilizzato l’immagine Docker ufficiale di TensorFlow Serving come base, ma è possibile utilizzare anche quelle che hai costruito dal codice sorgente.
Nota: TensorFlow Serving trae vantaggio dalle ottimizzazioni hardware che sfruttano set di istruzioni come AVX512. Questi set di istruzioni possono velocizzare l’inferenza dei modelli di deep learning. Pertanto, se conosci l’hardware su cui il modello verrà distribuito, è spesso vantaggioso ottenere una versione ottimizzata dell’immagine di TensorFlow Serving e utilizzarla in tutto.
Ora che il container in esecuzione ha tutti i file necessari nella struttura delle directory appropriata, è necessario creare una nuova immagine Docker che includa queste modifiche. Ciò può essere fatto con il comando docker commit
di seguito, e avrai una nuova immagine Docker chiamata $NEW_IMAGE
. Una cosa importante da notare è che è necessario impostare la variabile d’ambiente MODEL_NAME
sul nome del modello, che in questo caso è hf-vit
. Questo dice a TensorFlow Serving quale modello distribuire.
$ NEW_IMAGE=tfserving:$MODEL_NAME
$ docker commit \
--change "ENV MODEL_NAME $MODEL_NAME" \
serving_base $NEW_IMAGE
Esecuzione dell’immagine Docker in locale
Infine, è possibile eseguire l’immagine Docker appena creata in locale per verificare che funzioni correttamente. Di seguito viene mostrato l’output del comando docker run
. Poiché l’output è verboso, l’abbiamo ridotto per concentrarci sui punti importanti. Inoltre, vale la pena notare che apre le porte 8500
e 8501
per le interfacce gRPC e HTTP/REST, rispettivamente.
$ docker run -p 8500:8500 -p 8501:8501 -t $NEW_IMAGE &
---------OUTPUT---------
(Re-)adding model: hf-vit
Successfully reserved resources to load servable {name: hf-vit version: 1}
Approving load for servable version {name: hf-vit version: 1}
Loading servable version {name: hf-vit version: 1}
Reading SavedModel from: /models/hf-vit/1
Reading SavedModel debug info (if present) from: /models/hf-vit/1
Successfully loaded servable version {name: hf-vit version: 1}
Running gRPC ModelServer at 0.0.0.0:8500 ...
Exporting HTTP/REST API at:localhost:8501 ...
Caricamento dell’immagine Docker
Il passaggio finale consiste nel caricare l’immagine Docker in un repository di immagini. Utilizzerai Google Container Registry (GCR) per questo scopo. Le seguenti righe di codice possono fare questo per te:
$ GCP_PROJECT_ID=<GCP_PROJECT_ID>
$ GCP_IMAGE=gcr.io/$GCP_PROJECT_ID/$NEW_IMAGE
$ gcloud auth configure-docker
$ docker tag $NEW_IMAGE $GCP_IMAGE
$ docker push $GCP_IMAGE
Dato che stiamo usando GCR, è necessario aggiungere il prefisso (nota anche gli altri formati) al tag dell’immagine Docker con gcr.io/<GCP_PROJECT_ID>
. Con l’immagine Docker preparata e caricata su GCR, puoi procedere con il suo deployment su un cluster Kubernetes.
Il deployment su un cluster Kubernetes richiede quanto segue:
-
Provisionare un cluster Kubernetes, eseguito con Google Kubernetes Engine (GKE) in questo post. Tuttavia, puoi anche utilizzare altre piattaforme e strumenti come EKS o Minikube.
-
Connettersi al cluster Kubernetes per eseguire un deployment.
-
Scrivere i manifesti YAML.
-
Eseguire il deployment con i manifesti utilizzando lo strumento di utilità
kubectl
.
Analizziamo ognuno di questi passaggi.
Provisioning di un cluster Kubernetes su GKE
Puoi usare uno script shell come questo (disponibile qui):
$ GKE_CLUSTER_NAME=tfs-cluster
$ GKE_CLUSTER_ZONE=us-central1-a
$ NUM_NODES=2
$ MACHINE_TYPE=n1-standard-8
$ gcloud container clusters create $GKE_CLUSTER_NAME \
--zone=$GKE_CLUSTER_ZONE \
--machine-type=$MACHINE_TYPE \
--num-nodes=$NUM_NODES
GCP offre una varietà di tipi di macchine per configurare il deployment come desideri. Ti incoraggiamo a fare riferimento alla documentazione per saperne di più.
Una volta che il cluster è stato creato, devi connetterti ad esso per eseguire il deployment. Poiché qui viene utilizzato GKE, è necessario autenticarsi. Puoi usare uno script shell come questo per fare entrambe queste operazioni:
$ GCP_PROJECT_ID=<ID_PROGETTO_GCP>
$ export USE_GKE_GCLOUD_AUTH_PLUGIN=True
$ gcloud container clusters get-credentials $GKE_CLUSTER_NAME \
--zone $GKE_CLUSTER_ZONE \
--project $GCP_PROJECT_ID
Il comando gcloud container clusters get-credentials
si occupa sia di connettersi al cluster che di autenticarsi. Una volta fatto ciò, sei pronto per scrivere i manifesti.
Scrittura dei manifesti di Kubernetes
I manifesti di Kubernetes sono scritti in file YAML. Sebbene sia possibile utilizzare un singolo file manifesto per eseguire il deployment, creare file manifest separati è spesso vantaggioso per delegare la separazione delle responsabilità. È comune utilizzare tre file manifest per raggiungere questo obiettivo:
-
deployment.yaml
definisce lo stato desiderato del Deployment fornendo il nome dell’immagine Docker, gli argomenti aggiuntivi durante l’esecuzione dell’immagine Docker, le porte da aprire per gli accessi esterni e i limiti delle risorse. -
service.yaml
definisce le connessioni tra i clienti esterni e i Pod all’interno del cluster Kubernetes. -
hpa.yaml
definisce le regole per scalare verso l’alto e verso il basso il numero di Pod che compongono il Deployment, ad esempio la percentuale di utilizzo della CPU.
Puoi trovare i manifesti pertinenti per questo post qui . Di seguito, presentiamo una panoramica grafica di come vengono utilizzati questi manifesti.
Successivamente, analizziamo le parti importanti di ciascuno di questi manifesti.
deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: tfs-server
name: tfs-server
...
spec:
containers:
- image: gcr.io/$GCP_PROJECT_ID/tfserving-hf-vit:latest
name: tfs-k8s
imagePullPolicy: Always
args: ["--tensorflow_inter_op_parallelism=2",
"--tensorflow_intra_op_parallelism=8"]
ports:
- containerPort: 8500
name: grpc
- containerPort: 8501
name: restapi
resources:
limits:
cpu: 800m
requests:
cpu: 800m
...
Puoi configurare i nomi come tfs-server
, tfs-k8s
nel modo che preferisci. Sotto containers
, specifica l’URI dell’immagine Docker che il deployment utilizzerà. L’utilizzo delle risorse correnti viene monitorato impostando i limiti consentiti delle resources
per il container. Ciò consente all’Autoscaler di Pod Orizzontale (discusso in seguito) di decidere se aumentare o diminuire il numero di container. requests.cpu
è la quantità minima di risorse CPU necessarie per far funzionare correttamente il container ed è impostata dagli operatori. Qui 800m significa l’80% della risorsa CPU totale. Quindi, l’Autoscaler di Pod Orizzontale monitora l’utilizzo medio della CPU come somma di requests.cpu
in tutti i Pod per prendere decisioni di scalabilità.
Oltre alla configurazione specifica di Kubernetes, puoi specificare opzioni specifiche di TensorFlow Serving in args
. In questo caso, ne hai due:
-
tensorflow_inter_op_parallelism
, che imposta il numero di thread per eseguire in parallelo operazioni indipendenti. Il valore consigliato per questo parametro è 2. -
tensorflow_intra_op_parallelism
, che imposta il numero di thread per eseguire in parallelo operazioni individuali. Il valore consigliato è il numero di core fisici della CPU del deployment.
Puoi saperne di più su queste opzioni (e altre) e suggerimenti su come ottimizzarle per la distribuzione da qui e da qui .
service.yaml
:
apiVersion: v1
kind: Service
metadata:
labels:
app: tfs-server
name: tfs-server
spec:
ports:
- port: 8500
protocol: TCP
targetPort: 8500
name: tf-serving-grpc
- port: 8501
protocol: TCP
targetPort: 8501
name: tf-serving-restapi
selector:
app: tfs-server
type: LoadBalancer
Abbiamo impostato il tipo di servizio su ‘LoadBalancer’ in modo che gli endpoint siano esposti esternamente al cluster Kubernetes. Seleziona il Deployment ‘tfs-server’ per stabilire connessioni con client esterni tramite le porte specificate. Apriamo due porte, ‘8500’ e ‘8501’, rispettivamente per le connessioni gRPC e HTTP/REST.
hpa.yaml
:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: tfs-server
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: tfs-server
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 80
HPA sta per H orizontal P od A utoscaler. Imposta criteri per decidere quando scalare il numero di Pods nel Deployment di destinazione. Puoi saperne di più sull’algoritmo di autoscaling utilizzato internamente da Kubernetes qui .
Qui specifici come Kubernetes dovrebbe gestire l’autoscaling. In particolare, definisci il limite di repliche entro cui eseguire l’autoscaling – minReplicas
e maxReplicas
– e l’utilizzo target della CPU. targetCPUUtilizationPercentage
è una metrica importante per l’autoscaling. Il seguente thread riassume bene cosa significa (tratto da qui ):
L’utilizzo della CPU è l’utilizzo medio della CPU di tutti i Pods in un deployment nell’ultimo minuto diviso dalla CPU richiesta di questo deployment. Se la media dell’utilizzo della CPU dei Pods è superiore al target che hai definito, le repliche verranno adattate.
Ricorda di specificare resources
nel manifesto del deployment. Specificando le resources
, il controllo Kubernetes inizia a monitorare le metriche, quindi il targetCPUUtilization
funziona. Altrimenti, HPA non conosce lo stato corrente del Deployment.
Puoi sperimentare e impostare questi numeri in base alle tue esigenze. Tieni presente, però, che l’autoscaling dipenderà dalla quota disponibile su GCP poiché GKE utilizza internamente Google Compute Engine per gestire queste risorse.
Eseguire la distribuzione
Una volta pronti i manifest, puoi applicarli al cluster Kubernetes attualmente connesso con il comando kubectl apply
.
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
$ kubectl apply -f hpa.yaml
Anche se è possibile utilizzare kubectl
per applicare ciascuno dei manifest per eseguire la distribuzione, può diventare complicato se hai molti manifest diversi. In questo caso, un’utilità come Kustomize può essere utile. Basta definire un’altra specifica chiamata kustomization.yaml
in questo modo:
commonLabels:
app: tfs-server
resources:
- deployment.yaml
- hpa.yaml
- service.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
Poi è sufficiente una riga per eseguire effettivamente la distribuzione:
$ kustomize build . | kubectl apply -f -
Istruzioni complete sono disponibili qui . Una volta eseguita la distribuzione, è possibile recuperare l’indirizzo IP dell’endpoint in questo modo:
$ kubectl rollout status deployment/tfs-server
$ kubectl get svc tfs-server --watch
---------OUTPUT---------
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
tfs-server LoadBalancer xxxxxxxxxx xxxxxxxxxx 8500:30869/TCP,8501:31469/TCP xxx
Annota l’indirizzo IP esterno quando diventa disponibile.
E questo riassume tutti i passaggi necessari per distribuire il tuo modello su Kubernetes! Kubernetes fornisce elegantemente astrazioni per elementi complessi come l’autoscaling e la gestione dei cluster, consentendoti di concentrarti sugli aspetti cruciali a cui dovresti prestare attenzione durante la distribuzione di un modello. Questi includono l’utilizzo delle risorse, la sicurezza (che non abbiamo trattato qui), le stelle polari delle prestazioni come la latenza, ecc.
Dato che hai ottenuto un indirizzo IP esterno per il punto di accesso, puoi utilizzare la seguente lista per testarlo:
import tensorflow as tf
import json
import base64
image_path = tf.keras.utils.get_file(
"image.jpg", "http://images.cocodataset.org/val2017/000000039769.jpg"
)
bytes_inputs = tf.io.read_file(image_path)
b64str = base64.urlsafe_b64encode(bytes_inputs.numpy()).decode("utf-8")
data = json.dumps(
{"signature_name": "serving_default", "instances": [b64str]}
)
json_response = requests.post(
"http://<ENDPOINT-IP>:8501/v1/models/hf-vit:predict",
headers={"content-type": "application/json"},
data=data
)
print(json.loads(json_response.text))
---------OUTPUT---------
{'predictions': [{'label': 'Gatto egiziano', 'confidence': 0.896659195}]}
Se sei interessato a sapere come si comporterebbe questa distribuzione se dovesse gestire più traffico, ti consigliamo di leggere questo articolo . Consulta il repository corrispondente per saperne di più su come eseguire test di carico con Locust e visualizzare i risultati.
TensorFlow Serving offre varie opzioni per personalizzare la distribuzione in base al caso d’uso dell’applicazione. Di seguito, discutiamo brevemente alcune di esse.
enable_batching
abilita la capacità di inferenza batch che raccoglie le richieste in arrivo in un determinato intervallo di tempo, le raggruppa in un batch, esegue una inferenza batch e restituisce i risultati di ciascuna richiesta ai client appropriati. TensorFlow Serving fornisce un ricco insieme di opzioni configurabili (come max_batch_size
, num_batch_threads
) per adattare le tue esigenze di distribuzione. Puoi saperne di più su di esse qui . Il batching è particolarmente vantaggioso per applicazioni in cui non è necessario ottenere previsioni da un modello istantaneamente. In questi casi, di solito si raccolgono insieme più campioni per la previsione in batch e quindi si inviano tali batch per la previsione. Per nostra fortuna, TensorFlow Serving può configurare tutto questo automaticamente quando abilitiamo le sue capacità di batching.
enable_model_warmup
riscalda alcuni dei componenti di TensorFlow che vengono istanziati in modo pigro con dati di input fittizi. In questo modo, puoi assicurarti che tutto sia correttamente caricato e che non ci siano ritardi durante il tempo effettivo del servizio.
In questo post e nel repository associato , hai imparato a distribuire il modello Vision Transformer di 🤗 Transformers su un cluster Kubernetes. Se lo stai facendo per la prima volta, i passaggi potrebbero sembrare un po’ intimidatori, ma una volta compresi, diventeranno presto un componente essenziale del tuo set di strumenti. Se eri già familiare con questo flusso di lavoro, speriamo che questo post ti sia comunque stato utile.
Abbiamo applicato lo stesso flusso di lavoro di distribuzione per una versione ottimizzata ONNX del medesimo modello Vision Transformer. Per ulteriori dettagli, consulta questo link . I modelli ottimizzati ONNX sono particolarmente vantaggiosi se si utilizzano CPU x86 per la distribuzione.
Nel prossimo post, ti mostreremo come eseguire queste distribuzioni con significativamente meno codice utilizzando Vertex AI, più o meno come model.deploy(autoscaling_config=...)
e boom! Speriamo che tu sia altrettanto entusiasta di noi.
Grazie al team del programma ML Developer Relations di Google, che ci ha fornito crediti GCP per condurre gli esperimenti.