Implementando 馃 ViT en Kubernetes con TF Serving
'Implementing 馃 ViT in Kubernetes with TF Serving'
En el post anterior, mostramos c贸mo implementar un modelo Vision Transformer (ViT) de 馃 Transformers localmente con TensorFlow Serving. Cubrimos temas como la preprocesamiento y postprocesamiento de incrustaciones dentro del modelo Vision Transformer, manejo de solicitudes gRPC y m谩s.
Aunque las implementaciones locales son un excelente punto de partida para construir algo 煤til, necesitar铆as realizar implementaciones que puedan atender a muchos usuarios en proyectos reales. En este post, aprender谩s c贸mo escalar la implementaci贸n local del post anterior con Docker y Kubernetes. Por lo tanto, asumimos cierta familiaridad con Docker y Kubernetes.
Este post se basa en el post anterior, por lo que recomendamos encarecidamente leerlo primero. Puedes encontrar todo el c贸digo discutido a lo largo de este post en este repositorio.
El flujo de trabajo b谩sico para escalar una implementaci贸n como la nuestra incluye los siguientes pasos:
-
Contenerizar la l贸gica de la aplicaci贸n: La l贸gica de la aplicaci贸n implica un modelo servido que puede manejar solicitudes y devolver predicciones. Para la contenerizaci贸n, Docker es la opci贸n est谩ndar de la industria.
-
Implementar el contenedor Docker: Aqu铆 tienes varias opciones. La opci贸n m谩s utilizada es implementar el contenedor Docker en un cl煤ster de Kubernetes. Kubernetes proporciona numerosas caracter铆sticas amigables para la implementaci贸n (por ejemplo, escalado autom谩tico y seguridad). Puedes utilizar una soluci贸n como Minikube para gestionar cl煤steres de Kubernetes localmente o una soluci贸n sin servidor como Elastic Kubernetes Service (EKS).
Es posible que te preguntes por qu茅 utilizar una configuraci贸n expl铆cita como esta en la era de Sagemaker, Vertex AI, que proporciona caracter铆sticas espec铆ficas de implementaci贸n de ML desde el principio. Es justo pensarlo.
El flujo de trabajo anterior es ampliamente adoptado en la industria y muchas organizaciones se benefician de 茅l. Ya ha sido probado en batalla durante muchos a帽os. Tambi茅n te permite tener un control m谩s granular de tus implementaciones al abstraer los aspectos no triviales.
Este post utiliza Google Kubernetes Engine (GKE) para aprovisionar y gestionar un cl煤ster de Kubernetes. Asumimos que ya tienes un proyecto de GCP habilitado para facturaci贸n si est谩s utilizando GKE. Adem谩s, ten en cuenta que necesitar谩s configurar la utilidad gcloud
para realizar la implementaci贸n en GKE. Pero los conceptos discutidos en este post tambi茅n se aplican si decides utilizar Minikube.
Nota: Los fragmentos de c贸digo mostrados en este post se pueden ejecutar en una terminal Unix siempre y cuando hayas configurado la utilidad gcloud
junto con Docker y kubectl
. Se encuentran disponibles m谩s instrucciones en el repositorio adjunto.
El modelo de servicio puede manejar entradas de im谩genes sin procesar como bytes y es capaz de preprocesamiento y postprocesamiento.
En esta secci贸n, ver谩s c贸mo contenerizar ese modelo utilizando la imagen base de TensorFlow Serving. TensorFlow Serving consume modelos en formato SavedModel
. Recuerda c贸mo obtuviste dicho SavedModel
en el post anterior. Asumimos que tienes el SavedModel
comprimido en formato tar.gz
. Puedes obtenerlo aqu铆 por si acaso. Luego, el SavedModel
debe colocarse en la estructura de directorios especial de <NOMBRE_MODELO>/<VERSION>/<SavedModel>
. De esta manera, TensorFlow Serving gestiona simult谩neamente m煤ltiples implementaciones de modelos con diferentes versiones.
Preparando la imagen de Docker
El script de shell a continuaci贸n coloca el SavedModel
en hf-vit/1
dentro del directorio principal models. Copiar谩s todo lo que hay dentro de 茅l al preparar la imagen de Docker. En este ejemplo, solo hay un modelo, pero este enfoque es m谩s generalizable.
$ 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
A continuaci贸n, mostramos c贸mo est谩 estructurado el directorio models
en nuestro 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
La imagen personalizada de TensorFlow Serving debe construirse sobre la base de la imagen base. Hay varios enfoques para esto, pero lo har谩s ejecutando un contenedor Docker como se ilustra en el documento oficial. Comenzamos ejecutando la imagen tensorflow/serving
en modo de segundo plano, luego se copia el directorio completo models
al contenedor en ejecuci贸n de la siguiente manera.
$ docker run -d --name serving_base tensorflow/serving
$ docker cp models/ serving_base:/models/
Utilizamos la imagen oficial de Docker de TensorFlow Serving como base, pero tambi茅n puedes utilizar las que hayas construido desde el c贸digo fuente.
Nota: TensorFlow Serving se beneficia de optimizaciones de hardware que aprovechan conjuntos de instrucciones como AVX512. Estos conjuntos de instrucciones pueden acelerar la inferencia de modelos de aprendizaje profundo. Por lo tanto, si conoces el hardware en el que se implementar谩 el modelo, a menudo es beneficioso obtener una versi贸n optimizada de la imagen de TensorFlow Serving y utilizarla en todo momento.
Ahora que el contenedor en ejecuci贸n tiene todos los archivos necesarios en la estructura de directorios adecuada, debemos crear una nueva imagen de Docker que incluya estos cambios. Esto se puede hacer con el siguiente comando docker commit
, y tendr谩s una nueva imagen de Docker llamada $NEW_IMAGE
. Una cosa importante a tener en cuenta es que debes establecer la variable de entorno MODEL_NAME
en el nombre del modelo, que en este caso es hf-vit
. Esto le indica a TensorFlow Serving qu茅 modelo implementar.
$ NEW_IMAGE=tfserving:$MODEL_NAME
$ docker commit \
--change "ENV MODEL_NAME $MODEL_NAME" \
serving_base $NEW_IMAGE
Ejecutando la imagen de Docker localmente
Por 煤ltimo, puedes ejecutar la imagen de Docker reci茅n creada localmente para comprobar si funciona correctamente. A continuaci贸n, se muestra la salida del comando docker run
. Dado que la salida es detallada, la hemos reducido para centrarnos en lo importante. Adem谩s, cabe se帽alar que abre los puertos 8500
y 8501
para los puntos finales gRPC y HTTP/REST, respectivamente.
$ docker run -p 8500:8500 -p 8501:8501 -t $NEW_IMAGE &
---------SALIDA---------
(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 ...
Subiendo la imagen de Docker
El 煤ltimo paso aqu铆 es subir la imagen de Docker a un repositorio de im谩genes. Utilizar谩s Google Container Registry (GCR) para este prop贸sito. Las siguientes l铆neas de c贸digo pueden hacer esto por ti:
$ 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
Dado que estamos utilizando GCR, debes agregar el prefijo de la etiqueta de la imagen de Docker (nota tambi茅n otros formatos) con gcr.io/<GCP_PROJECT_ID>
. Con la imagen de Docker preparada y subida a GCR, ahora puedes proceder a implementarla en un cl煤ster de Kubernetes.
La implementaci贸n en un cl煤ster de Kubernetes requiere lo siguiente:
-
Provisionar un cl煤ster de Kubernetes, que se realiza con Google Kubernetes Engine (GKE) en esta publicaci贸n. Sin embargo, tambi茅n puedes utilizar otras plataformas y herramientas como EKS o Minikube.
-
Conectarse al cl煤ster de Kubernetes para realizar una implementaci贸n.
-
Escribir manifiestos YAML.
-
Realizar la implementaci贸n con los manifiestos utilizando la herramienta
kubectl
.
Repasemos cada uno de estos pasos.
Provisi贸n de un cl煤ster de Kubernetes en GKE
Puedes usar un script de shell como este para esto (disponible aqu铆):
$ 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 ofrece una variedad de tipos de m谩quinas para configurar la implementaci贸n de la manera que desees. Te recomendamos que consultes la documentaci贸n para obtener m谩s informaci贸n al respecto.
Una vez que se provisiona el cl煤ster, debes conectarte a 茅l para realizar la implementaci贸n. Dado que se utiliza GKE aqu铆, tambi茅n necesitas autenticarte. Puedes usar un script de shell como este para hacer ambas cosas:
$ GCP_PROJECT_ID=<GCP_PROJECT_ID>
$ export USE_GKE_GCLOUD_AUTH_PLUGIN=True
$ gcloud container clusters get-credentials $GKE_CLUSTER_NAME \
--zone $GKE_CLUSTER_ZONE \
--project $GCP_PROJECT_ID
El comando gcloud container clusters get-credentials
se encarga tanto de conectarse al cl煤ster como de la autenticaci贸n. Una vez hecho esto, est谩s listo para escribir los manifiestos.
Escribiendo manifiestos de Kubernetes
Los manifiestos de Kubernetes se escriben en archivos YAML. Si bien es posible utilizar un solo archivo de manifiesto para realizar la implementaci贸n, crear archivos de manifiesto separados a menudo es beneficioso para delegar la separaci贸n de responsabilidades. Es com煤n utilizar tres archivos de manifiesto para lograr esto:
-
deployment.yaml
define el estado deseado de la implementaci贸n proporcionando el nombre de la imagen Docker, argumentos adicionales al ejecutar la imagen Docker, los puertos para abrir para accesos externos y los l铆mites de recursos. -
service.yaml
define las conexiones entre clientes externos y los Pods dentro del cl煤ster de Kubernetes. -
hpa.yaml
define reglas para escalar hacia arriba y hacia abajo el n煤mero de Pods que forman parte de la implementaci贸n, como el porcentaje de utilizaci贸n de la CPU.
Puedes encontrar los manifiestos relevantes para esta publicaci贸n aqu铆. A continuaci贸n, presentamos una visi贸n general pict贸rica de c贸mo se consumen estos manifiestos.
A continuaci贸n, revisamos las partes importantes de cada uno de estos manifiestos.
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
...
Puedes configurar los nombres como tfs-server
, tfs-k8s
de la forma que desees. Bajo containers
, especificas la URI de la imagen Docker que utilizar谩 la implementaci贸n. La utilizaci贸n actual de recursos se monitorea estableciendo los l铆mites permitidos de los resources
para el contenedor. Esto le permite al escalador de pod horizontal (discutido m谩s adelante) decidir escalar hacia arriba o hacia abajo el n煤mero de contenedores. requests.cpu
es la cantidad m铆nima de recursos de CPU necesarios para que el contenedor funcione correctamente, establecido por los operadores. Aqu铆, 800m significa el 80% de todos los recursos de la CPU. Por lo tanto, el escalador de pod horizontal monitorea la utilizaci贸n promedio de la CPU a partir de la suma de requests.cpu
en todos los Pods para tomar decisiones de escalado.
Adem谩s de la configuraci贸n espec铆fica de Kubernetes, puedes especificar opciones espec铆ficas de TensorFlow Serving en args
. En este caso, tienes dos opciones:
-
tensorflow_inter_op_parallelism
, que establece el n煤mero de hilos para ejecutar operaciones independientes en paralelo. El valor recomendado para esto es 2. -
tensorflow_intra_op_parallelism
, que establece el n煤mero de hilos para ejecutar operaciones individuales en paralelo. El valor recomendado es el n煤mero de n煤cleos f铆sicos de la CPU de implementaci贸n.
Puedes obtener m谩s informaci贸n sobre estas opciones (y otras) y consejos sobre c贸mo ajustarlas para su implementaci贸n desde aqu铆 y aqu铆.
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
Hemos configurado el tipo de servicio como ‘LoadBalancer’ para que los puntos de conexi贸n sean accesibles externamente desde el cl煤ster de Kubernetes. Se selecciona el despliegue ‘tfs-server’ para establecer conexiones con clientes externos a trav茅s de los puertos especificados. Abrimos dos puertos, ‘8500’ y ‘8501’, para conexiones gRPC y HTTP/REST respectivamente.
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 significa H orizontal P od A utoscaler. Establece los criterios para decidir cu谩ndo escalar el n煤mero de Pods en el despliegue objetivo. Puedes obtener m谩s informaci贸n sobre el algoritmo de escalado autom谩tico utilizado internamente por Kubernetes aqu铆.
Aqu铆 se especifica c贸mo Kubernetes debe manejar el escalado autom谩tico. En particular, se define el l铆mite de r茅plicas dentro del cual debe realizar el escalado autom谩tico, minReplicas
y maxReplicas
, y la utilizaci贸n objetivo de la CPU. targetCPUUtilizationPercentage
es una m茅trica importante para el escalado autom谩tico. El siguiente enlace resume adecuadamente su significado (tomado de aqu铆):
La utilizaci贸n de la CPU es el uso promedio de la CPU de todos los Pods en un despliegue en el 煤ltimo minuto, dividido por la CPU solicitada de este despliegue. Si la media de la utilizaci贸n de la CPU de los Pods es mayor que el objetivo que has definido, se ajustar谩n tus r茅plicas.
Recuerda especificar resources
en el manifiesto de despliegue. Al especificar los resources
, el plano de control de Kubernetes comienza a supervisar las m茅tricas, por lo que el targetCPUUtilization
funciona. De lo contrario, HPA no conoce el estado actual del despliegue.
Puedes experimentar y establecer estos n煤meros seg煤n tus necesidades. Sin embargo, el escalado autom谩tico depender谩 de la cuota disponible en GCP, ya que GKE utiliza internamente Google Compute Engine para gestionar estos recursos.
Realizando el despliegue
Una vez que los manifiestos est茅n listos, puedes aplicarlos al cl煤ster de Kubernetes actualmente conectado con el comando kubectl apply
.
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
$ kubectl apply -f hpa.yaml
Aunque usar kubectl
est谩 bien para aplicar cada uno de los manifiestos y realizar el despliegue, puede volverse complicado r谩pidamente si tienes muchos manifiestos diferentes. Aqu铆 es donde puede ser 煤til una utilidad como Kustomize. Simplemente defines otra especificaci贸n llamada kustomization.yaml
de la siguiente manera:
commonLabels:
app: tfs-server
resources:
- deployment.yaml
- hpa.yaml
- service.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
Entonces, solo necesitas una l铆nea para realizar el despliegue real:
$ kustomize build . | kubectl apply -f -
Las instrucciones completas est谩n disponibles aqu铆. Una vez que se haya realizado el despliegue, puedes obtener la direcci贸n IP del punto de conexi贸n de la siguiente manera:
$ 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
Toma nota de la IP externa cuando est茅 disponible.
隆Y eso resume todos los pasos que necesitas para implementar tu modelo en Kubernetes! Kubernetes proporciona de manera elegante abstracciones para aspectos complejos como el escalado autom谩tico y la gesti贸n de cl煤steres, mientras te permite centrarte en los aspectos cruciales en los que debes enfocarte al implementar un modelo. Estos incluyen la utilizaci贸n de recursos, la seguridad (no la cubrimos aqu铆), los principales indicadores de rendimiento como la latencia, etc.
Dado que obtuviste una IP externa para el endpoint, puedes utilizar la siguiente lista para probarlo:
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))
---------SALIDA---------
{'predictions': [{'label': 'Gato egipcio', 'confidence': 0.896659195}]}
Si est谩s interesado en saber c贸mo se desempe帽ar铆a esta implementaci贸n si recibe m谩s tr谩fico, te recomendamos que consultes este art铆culo . Consulta el repositorio correspondiente para obtener m谩s informaci贸n sobre c贸mo ejecutar pruebas de carga con Locust y visualizar los resultados.
TensorFlow Serving ofrece varias opciones para adaptar la implementaci贸n seg煤n el caso de uso de tu aplicaci贸n. A continuaci贸n, discutimos brevemente algunas de ellas.
enable_batching
habilita la capacidad de inferencia por lotes que recopila las solicitudes entrantes durante un cierto intervalo de tiempo, las agrupa en un lote, realiza una inferencia por lotes y devuelve los resultados de cada solicitud a los clientes correspondientes. TensorFlow Serving proporciona un conjunto completo de opciones configurables (como max_batch_size
, num_batch_threads
) para adaptar tus necesidades de implementaci贸n. Puedes obtener m谩s informaci贸n sobre ellas aqu铆 . El procesamiento por lotes es especialmente beneficioso para aplicaciones en las que no necesitas predicciones de un modelo de forma instant谩nea. En esos casos, normalmente recopilar铆as varias muestras para la predicci贸n en lotes y luego enviar铆as esos lotes para la predicci贸n. Afortunadamente, TensorFlow Serving puede configurar todo esto autom谩ticamente cuando se habilitan sus capacidades de procesamiento por lotes.
enable_model_warmup
realiza un precalentamiento de algunos de los componentes de TensorFlow que se instancian de forma perezosa con datos de entrada ficticios. De esta manera, puedes asegurarte de que todo est茅 cargado correctamente y de que no haya retrasos durante el tiempo real del servicio.
En esta publicaci贸n y el repositorio asociado, aprendiste c贸mo implementar el modelo Vision Transformer de 馃 Transformers en un cl煤ster de Kubernetes. Si est谩s haciendo esto por primera vez, los pasos pueden parecer un poco desafiantes, pero una vez que los comprendas, pronto se convertir谩n en un componente esencial de tu conjunto de herramientas. Si ya estabas familiarizado con este flujo de trabajo, esperamos que esta publicaci贸n a煤n te haya sido 煤til.
Aplicamos el mismo flujo de trabajo de implementaci贸n para una versi贸n optimizada de ONNX del mismo modelo Vision Transformer. Para obtener m谩s detalles, consulta este enlace . Los modelos optimizados de ONNX son especialmente beneficiosos si est谩s utilizando CPUs x86 para la implementaci贸n.
En la pr贸xima publicaci贸n, te mostraremos c贸mo realizar estas implementaciones con significativamente menos c贸digo con Vertex AI, 隆m谩s como model.deploy(autoscaling_config=...)
y listo! Esperamos que est茅s tan emocionado como nosotros.
Gracias al equipo del Programa de Relaciones con Desarrolladores de ML en Google, que nos proporcion贸 cr茅ditos de GCP para realizar los experimentos.