TensorFlow Serving es el servidor de inferencia de TensorFlow para producción: carga modelos en formato SavedModel, los expone mediante APIs REST y gRPC y detecta nuevas versiones en disco sin reiniciar. En este tutorial lo desplegarás con la imagen oficial de Docker en Ubuntu 24.04, exportarás un modelo de ejemplo, lo consultarás por REST y configurarás el servidor para mantener dos versiones del modelo activas a la vez. Al final verás cómo habilitar el agrupamiento de peticiones y la variante con GPU.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS de 64 bits en arquitectura x86_64, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM. La imagen oficial
tensorflow/servingsolo se publica para x86_64. - Un usuario no root con privilegios
sudoy que pertenezca al grupodocker. - Docker Engine instalado desde el repositorio oficial de Docker.
- Unos 5 GB libres en disco para las imágenes de TensorFlow y TensorFlow Serving.
Paso 1: Descargar la imagen de TensorFlow Serving
La forma recomendada de ejecutar TensorFlow Serving es la imagen oficial de Docker Hub, que incluye el binario tensorflow_model_server ya compilado. Descárgala:
docker pull tensorflow/serving
Comprueba que la imagen está disponible y qué versión del servidor contiene:
docker run --rm --entrypoint tensorflow_model_server tensorflow/serving --version
TensorFlow ModelServer: 2.19.0
TensorFlow Library: 2.19.0
Paso 2: Exportar un modelo en formato SavedModel
TensorFlow Serving solo carga modelos en formato SavedModel y espera una estructura de directorios concreta: una carpeta por modelo y, dentro, una subcarpeta numérica por versión.
Para un modelo llamado half_plus_two, la versión 1 debe quedar así:
~/models/half_plus_two/1/saved_model.pb: el grafo y las firmas del modelo.~/models/half_plus_two/1/variables/: los pesos.
El servidor ignora los directorios cuyo nombre no es un número, y trata el número como versión del modelo.
Para no depender de un modelo entrenado, exportarás un modelo trivial que calcula y = x * 0.5 + 2. Así podrás verificar que las predicciones son exactas. Crea el directorio de trabajo y el script de exportación:
mkdir -p ~/models ~/tfs
nano ~/tfs/exportar.py
import sys
import tensorflow as tf
class HalfPlusTwo(tf.Module):
def __init__(self, offset):
super().__init__()
self.offset = tf.Variable(offset, dtype=tf.float32)
@tf.function(input_signature=[tf.TensorSpec(shape=[None], dtype=tf.float32, name="x")])
def __call__(self, x):
return {"y": x * 0.5 + self.offset}
destino = sys.argv[1]
offset = float(sys.argv[2])
modelo = HalfPlusTwo(offset)
tf.saved_model.save(
modelo,
destino,
signatures={"serving_default": modelo.__call__},
)
print(f"Modelo exportado en {destino} con offset {offset}")
La firma serving_default es la que TensorFlow Serving usa por defecto; define una entrada x (vector de floats) y una salida y. Con un modelo de Keras 3 obtendrías lo mismo con model.export("ruta/1").
El script recibe dos argumentos: el directorio de destino y el offset. En lugar de instalar TensorFlow en el servidor, ejecútalo con la imagen oficial tensorflow/tensorflow. La opción -u hace que los archivos creados pertenezcan a tu usuario:
docker run --rm -u "$(id -u):$(id -g)" \
-v "$HOME/models:/models" \
-v "$HOME/tfs:/tfs:ro" \
tensorflow/tensorflow python /tfs/exportar.py /models/half_plus_two/1 2.0
Modelo exportado en /models/half_plus_two/1 con offset 2.0
Inspecciona la firma del modelo con saved_model_cli, incluido en la misma imagen. Es la forma de saber qué nombres, tipos y formas espera el servidor:
docker run --rm -v "$HOME/models:/models:ro" tensorflow/tensorflow \
saved_model_cli show --dir /models/half_plus_two/1 --tag_set serve --signature_def serving_default
The given SavedModel SignatureDef contains the following input(s):
inputs['x'] tensor_info:
dtype: DT_FLOAT
shape: (-1)
name: serving_default_x:0
The given SavedModel SignatureDef contains the following output(s):
outputs['y'] tensor_info:
dtype: DT_FLOAT
shape: (-1)
name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict
Paso 3: Arrancar TensorFlow Serving
La imagen arranca el servidor con el modelo indicado en la variable MODEL_NAME, que busca en /models/<MODEL_NAME>. El puerto 8501 es la API REST y el 8500 la API gRPC. Publícalos solo en 127.0.0.1, porque los puertos publicados por Docker se saltan las reglas de UFW:
docker run -d --name tfserving --restart unless-stopped \
-p 127.0.0.1:8500:8500 \
-p 127.0.0.1:8501:8501 \
-v "$HOME/models/half_plus_two:/models/half_plus_two:ro" \
-e MODEL_NAME=half_plus_two \
tensorflow/serving
Revisa el log de arranque:
docker logs tfserving 2>&1 | tail -n 5
... Successfully loaded servable version {name: half_plus_two version: 1}
... Running gRPC ModelServer at 0.0.0.0:8500 ...
... Exporting HTTP/REST API at:localhost:8501 ...
Consulta el estado del modelo a través de la API:
curl http://127.0.0.1:8501/v1/models/half_plus_two
{
"model_version_status": [
{
"version": "1",
"state": "AVAILABLE",
"status": {
"error_code": "OK",
"error_message": ""
}
}
]
}
El estado AVAILABLE indica que la versión 1 está cargada y lista.
Paso 4: Hacer predicciones con la API REST
El endpoint de predicción es /v1/models/<nombre>:predict. El formato instances envía una lista de ejemplos; como el modelo tiene una sola entrada, cada elemento es un valor de x:
curl -X POST http://127.0.0.1:8501/v1/models/half_plus_two:predict \
-d '{"instances": [1.0, 2.0, 5.0]}'
{
"predictions": [2.5, 3.0, 4.5]
}
Los resultados coinciden con x * 0.5 + 2. Si el modelo tuviera varias entradas, cada instancia sería un objeto con una clave por nombre de entrada, por ejemplo {"x": 1.0, "z": 3.0}.
Desde Python basta con cualquier cliente HTTP. Por ejemplo, con requests:
import requests
respuesta = requests.post(
"http://127.0.0.1:8501/v1/models/half_plus_two:predict",
json={"instances": [1.0, 2.0, 5.0]},
timeout=10,
)
respuesta.raise_for_status()
print(respuesta.json()["predictions"])
La API gRPC del puerto 8500 ofrece menor latencia para cargas altas. Sus clientes usan el paquete de Python tensorflow-serving-api y los mismos nombres de entrada y salida que muestra saved_model_cli.
Paso 5: Publicar una nueva versión del modelo
TensorFlow Serving vigila el directorio del modelo y, por defecto, sirve siempre la versión con el número más alto. Si el servidor detecta el directorio de una versión mientras aún se está escribiendo, puede intentar cargar un modelo incompleto. Por eso conviene exportar primero a un directorio de preparación y moverlo después a su número definitivo: mv dentro del mismo sistema de archivos es atómico.
Exporta una versión con un offset distinto en ~/models/staging:
mkdir -p ~/models/staging
docker run --rm -u "$(id -u):$(id -g)" \
-v "$HOME/models:/models" \
-v "$HOME/tfs:/tfs:ro" \
tensorflow/tensorflow python /tfs/exportar.py /models/staging/half_plus_two 3.0
Publícala como versión 2:
mv ~/models/staging/half_plus_two ~/models/half_plus_two/2
En unos segundos el servidor la detecta, la carga y retira la versión 1. Compruébalo:
curl -s -X POST http://127.0.0.1:8501/v1/models/half_plus_two:predict \
-d '{"instances": [1.0]}'
{
"predictions": [3.5]
}
El resultado es ahora 1.0 * 0.5 + 3. Con docker logs tfserving verás las líneas de carga de la versión 2 y de descarga de la versión 1.
Paso 6: Servir varias versiones con un archivo de configuración
Para comparar versiones o hacer un despliegue gradual necesitas que ambas estén cargadas a la vez. Eso se configura con un archivo de configuración de modelos en formato texto de protobuf. Créalo dentro de ~/models:
nano ~/models/models.config
model_config_list {
config {
name: "half_plus_two"
base_path: "/models/half_plus_two"
model_platform: "tensorflow"
model_version_policy {
specific {
versions: 1
versions: 2
}
}
}
}
La política specific indica exactamente qué versiones cargar. Puedes añadir más bloques config para servir varios modelos distintos desde el mismo servidor.
Recrea el contenedor montando todo ~/models y pasando el archivo al servidor. --model_config_file_poll_wait_seconds=60 hace que relea el archivo cada minuto, de modo que los cambios se aplican sin reiniciar:
docker rm -f tfserving
docker run -d --name tfserving --restart unless-stopped \
-p 127.0.0.1:8500:8500 \
-p 127.0.0.1:8501:8501 \
-v "$HOME/models:/models:ro" \
tensorflow/serving \
--model_config_file=/models/models.config \
--model_config_file_poll_wait_seconds=60
Comprueba que ambas versiones están disponibles:
curl -s http://127.0.0.1:8501/v1/models/half_plus_two | grep -E '"version"|"state"'
"version": "2",
"state": "AVAILABLE",
"version": "1",
"state": "AVAILABLE",
Ahora puedes dirigir cada petición a una versión concreta con la ruta /versions/<n>:
curl -s -X POST http://127.0.0.1:8501/v1/models/half_plus_two/versions/1:predict -d '{"instances": [1.0]}'
curl -s -X POST http://127.0.0.1:8501/v1/models/half_plus_two/versions/2:predict -d '{"instances": [1.0]}'
La primera devuelve 2.5 y la segunda 3.5. Las peticiones sin versión van a la más alta de las cargadas.
Paso 7: Habilitar el agrupamiento de peticiones
Con muchas peticiones pequeñas y simultáneas, agrupar varias en un único lote aprovecha mucho mejor la CPU y, sobre todo, la GPU. Crea el archivo de parámetros:
nano ~/models/batching.config
max_batch_size { value: 32 }
batch_timeout_micros { value: 2000 }
num_batch_threads { value: 4 }
max_enqueued_batches { value: 100 }
max_batch_size: número máximo de ejemplos por lote.batch_timeout_micros: tiempo máximo que una petición espera a que se llene el lote. Es la latencia extra que aceptas a cambio de rendimiento.num_batch_threads: lotes procesados en paralelo; un valor cercano al número de núcleos es un buen punto de partida.
Recrea el contenedor con --enable_batching:
docker rm -f tfserving
docker run -d --name tfserving --restart unless-stopped \
-p 127.0.0.1:8500:8500 \
-p 127.0.0.1:8501:8501 \
-v "$HOME/models:/models:ro" \
tensorflow/serving \
--model_config_file=/models/models.config \
--model_config_file_poll_wait_seconds=60 \
--enable_batching=true \
--batching_parameters_file=/models/batching.config
Repite la petición del paso 6 para confirmar que el servidor sigue respondiendo. Ajusta los valores midiendo latencia y rendimiento con tu tráfico real.
Paso 8: Usar una GPU NVIDIA (opcional)
Si el servidor tiene una GPU NVIDIA, usa la imagen tensorflow/serving:latest-gpu. Necesitas el driver de NVIDIA (nvidia-smi debe funcionar en el host) y el NVIDIA Container Toolkit configurado para Docker. Con ambos instalados, añade --gpus all al comando:
docker rm -f tfserving
docker run -d --name tfserving --restart unless-stopped --gpus all \
-p 127.0.0.1:8500:8500 \
-p 127.0.0.1:8501:8501 \
-v "$HOME/models:/models:ro" \
tensorflow/serving:latest-gpu \
--model_config_file=/models/models.config
En el log de arranque (docker logs tfserving) debe aparecer una línea Created device /job:localhost/replica:0/task:0/device:GPU:0 con el nombre de la tarjeta.
Solución de problemas
El log muestra No versions of servable half_plus_two found under base path. El directorio montado no tiene la estructura <modelo>/<número>/saved_model.pb. Revisa la ruta con ls -R ~/models/half_plus_two y que el volumen de docker run apunta al directorio del modelo, no al de la versión.
La predicción devuelve un error de forma o de tipo. Los datos no coinciden con la firma. Compara la petición con la salida de saved_model_cli show del paso 2: nombres de entrada, dtype y número de dimensiones.
curl: (7) Failed to connect to 127.0.0.1 port 8501. El contenedor no está en marcha o ha fallado al cargar la configuración. Revisa docker ps -a y docker logs tfserving; un error de sintaxis en models.config impide arrancar el servidor.
Conclusión
Tienes TensorFlow Serving funcionando en Docker sobre Ubuntu 24.04, con un modelo servido por REST y gRPC, despliegue de nuevas versiones sin reinicio, varias versiones activas mediante un archivo de configuración y agrupamiento de peticiones. Como siguientes pasos, coloca un proxy inverso con TLS y autenticación delante del puerto 8501 si otros servicios deben consultarlo desde fuera, recoge métricas del servidor con --monitoring_config_file y automatiza la exportación de modelos desde tu pipeline de entrenamiento.
