Sistema de predicción de ganancias que ejecuta Ray Serve y Triton Server en el mismo contenedor, combinando flexibilidad y alto rendimiento con latencia mínima.
Esta arquitectura implementa Ray Serve y Triton Server dentro del mismo contenedor, donde Ray maneja el API y preprocesamiento, mientras Triton se encarga de la inferencia, comunicándose localmente sin overhead de red.
Cliente → [Contenedor único]
├─ Ray Serve (API) → Preprocesamiento
└─ Triton Server → ONNX Runtime
→ Respuesta
Componentes:
- Ray Serve: API REST, validación, preprocesamiento
- Triton Server: Motor de inferencia ONNX (proceso interno)
- Comunicación local: Ray y Triton en el mismo contenedor (localhost)
- Dockerfile único: Una sola imagen con ambos servicios
- Latencia mínima: Sin overhead de red entre servicios
- Balance perfecto: Flexibilidad de Ray + Performance de Triton
- Despliegue simple: Un solo pod/contenedor para gestionar
- Menor uso de recursos: Memoria compartida entre procesos
- Configuración simplificada: No requiere networking entre contenedores
- Debugging más fácil: Todo en un solo lugar
- Costo reducido: Menos pods = menos infraestructura
- Escalado acoplado: Ray y Triton escalan juntos (no independientemente)
- Recursos combinados: Pico de uso de ambos servicios simultáneo
- Tamaño de imagen: Imagen Docker grande (~10GB con Triton)
- Menos aislamiento: Problemas en Triton pueden afectar a Ray y viceversa
- Actualizaciones acopladas: Cambio en uno requiere redesplegar ambos
- ✅ Clusters con recursos limitados (menos pods)
- ✅ Proyectos que necesitan balance entre características
- ✅ Cuando la latencia de red entre servicios es crítica (<5ms)
- ✅ Simplificación operacional es prioritaria
- ✅ Equipos pequeños que manejan todo el stack
- ✅ Casos de uso donde Ray y Triton escalan similarmente
Los modelos no están incluidos en este repositorio. Debes obtenerlos y colocarlos en la estructura correcta.
ray+triton-combinados/
└── model_repository/
└── profit/
├── 1/
│ ├── model.onnx # ← DESCARGAR
│ ├── model.onnx.data # ← DESCARGAR (si aplica)
│ └── preprocessing_info.pkl # ← DESCARGAR
└── config.pbtxt # ← Ya incluido
# Descargar modelo ONNX y preprocessing info
gsutil cp gs://tu-bucket/models/serving/model.onnx \
model_repository/serving/1/
gsutil cp gs://tu-bucket/models/serving/model.onnx.data \
model_repository/serving/1/
gsutil cp gs://tu-bucket/models/serving/preprocessing_info.pkl \
model_repository/serving/1/
# O desde S3 (AWS)
aws s3 cp s3://tu-bucket/models/serving/model.onnx \
model_repository/serving/1/
aws s3 cp s3://tu-bucket/models/serving/model.onnx.data \
model_repository/serving/1/
aws s3 cp s3://tu-bucket/models/serving/preprocessing_info.pkl \
model_repository/serving/1/import torch.onnx
import pickle
# 1. Exportar modelo ONNX
torch.onnx.export(
model,
dummy_input,
"model_repository/serving/1/model.onnx",
input_names=['input'],
output_names=['output'],
dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}
)
# 2. Guardar preprocessing_info.pkl
preprocessing_info = {
'encoders': {...}, # LabelEncoders
'scaler': scaler, # StandardScaler
'feature_columns': [...], # Lista de 15 features
'categorical_columns': [...] # Lista de columnas categóricas
}
with open('model_repository/serving/1/preprocessing_info.pkl', 'wb') as f:
pickle.dump(preprocessing_info, f)Modelo ONNX:
- Input:
input- Tensor FP32 de forma[-1, 15] - Output:
output- Tensor FP32 de forma[-1, 1] - Opset: 11 o superior
- Backend: Compatible con ONNX Runtime
Preprocessing Info:
- Debe contener:
encoders,scaler,feature_columns,categorical_columns - Compatible con Scikit-learn
- Produce exactamente 15 features normalizadas
Este proyecto despliega modelos ONNX usando Triton Inference Server dentro de Ray Serve en Kubernetes (Kind).
ray+triton/
├── Dockerfile # Imagen con Triton Server + Ray Serve
├── requirements.txt # Dependencias Python (Ray, FastAPI, etc.)
├── serve.py # Deployment de Ray Serve con Triton
├── config-serve-docker.yaml # Configuración de RayService para Kubernetes
└── model_repository/ # Repositorio de modelos de Triton
├── mnist_onnx/
│ ├── 1/
│ │ └── model.onnx
│ └── config.pbtxt
├── profit/
│ ├── 1/
│ │ └── model.onnx
│ ├── config.pbtxt
│ └── model.onnx.data
└── profit/
├── 1/
│ └── model.onnx
└── config.pbtxt
- Docker
- Kind (Kubernetes in Docker)
- kubectl
- Helm
kind create cluster --name sandbox-cluster# Agregar el repositorio de Helm
helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm repo update
# Instalar el operador
helm install kuberay-operator kuberay/kuberay-operator --version 1.4.2# Construir la imagen con Triton Server + Ray Serve
docker build -t ray-triton-models:latest .⏱️ Nota: Este paso puede tardar 5-10 minutos la primera vez, ya que descarga la imagen base de Triton (~10GB).
# Cargar la imagen en el clúster de Kind
kind load docker-image ray-triton-models:latest --name sandbox-cluster⏱️ Nota: Este paso puede tardar 2-5 minutos dependiendo del tamaño de la imagen.
# Aplicar la configuración de RayService
kubectl apply -f config-serve-docker.yaml# Ver el estado de los pods
kubectl get pods -w
# Ver los logs del pod head
kubectl logs -f <pod-head-name> -c ray-head
# O usar k9s para monitoreo interactivo
k9sEspera a que los pods estén en estado Running y Ready (puede tardar 1-2 minutos).
# Eliminar el deployment actual, reconstruir y redesplegar
kubectl delete -f config-serve-docker.yaml && \
docker build -t ray-triton-models:latest . && \
kind load docker-image ray-triton-models:latest --name sandbox-cluster && \
kubectl apply -f config-serve-docker.yaml && \
kubectl get pods -w# Limpiar imágenes no usadas
docker image prune -a -f
# Limpiar volúmenes no usados
docker volume prune -f
# Limpieza completa (cuidado: elimina todo lo no usado)
docker system prune -a --volumes -f# Encontrar el servicio
kubectl get services
# Hacer port-forward
kubectl port-forward service/triton-sd-service-docker-serve-svc 8000:8000# Hacer una predicción con el modelo profit
curl -X POST http://localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{
"input": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
}'Nota: El modelo profit espera exactamente 70 features.
Ray Serve expone el siguiente endpoint en el puerto 8000:
| Endpoint | Método | Descripción |
|---|---|---|
/predict |
POST | Predicción de ganancias (principal) |
Realiza una predicción de ganancias usando el modelo ONNX ejecutado en Triton.
URL: http://localhost:8000/predict
Nota: Este endpoint espera 70 features preprocesadas, no datos raw como los otros módulos.
Request Body:
{
"input": [
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0
]
}Response (Success):
{
"status": "ok",
"model": "profit",
"scores": [2410.5]
}Response (Error - Wrong Features):
{
"status": "error",
"message": "Se esperaban exactamente 70 features, pero llegaron (15,)."
}Response (Error - Model Not Loaded):
{
"status": "error",
"message": "No se pudo cargar el modelo 'profit' en Triton."
}Ejemplo con curl:
curl -X POST http://localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{
"input": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0,
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
}'Ejemplo con Python:
import requests
url = "http://localhost:8000/predict"
# 70 features preprocesadas
features = [0.1] * 70 # Ejemplo simplificado
response = requests.post(url, json={"input": features})
result = response.json()
print(f"Status: {result['status']}")
if result['status'] == 'ok':
print(f"Prediction: {result['scores'][0]}")Triton Server está disponible internamente en localhost:8001 (gRPC) para comunicación con Ray Serve.
No se recomienda acceder directamente a Triton desde fuera del contenedor, pero si necesitas debuggear:
# Dentro del contenedor
curl http://localhost:8000/v2/health/ready # HTTP
curl http://localhost:8000/v2/models # Ver modelos- Input:
input- FP32, dims: [-1, 70] - Output:
variable- FP32, dims: [-1, 1] - Backend: ONNX Runtime
- Device: CPU
- Input:
input- FP32, dims: [-1, 15] - Output:
output- FP32, dims: [1, 1] - Backend: ONNX Runtime
- Device: CPU
- Input:
Input3- FP32, dims: [1, 1, 28, 28] - Output:
Plus214_Output_0- FP32, dims: [1, 10] - Backend: ONNX Runtime
- Device: CPU
# Ver los logs del pod que falla
kubectl logs <pod-name> -c ray-head
# Describir el pod para más detalles
kubectl describe pod <pod-name># Limpiar Docker
docker system prune -a --volumes -f
# Recrear el clúster de Kind
kind delete cluster --name sandbox-cluster
kind create cluster --name sandbox-cluster- Asegúrate de usar la imagen base correcta:
nvcr.io/nvidia/tritonserver:24.03-py3 - NO uses la imagen
-sdk(solo cliente) - NO incluyas
tritonserverenrequirements.txt
- Es normal durante el inicio (1-2 minutos)
- Espera a que Ray Serve termine de cargar
- Si persiste después de 3 minutos, revisa los logs
kubectl delete -f config-serve-docker.yamlkind delete cluster --name sandbox-clusterdocker rmi ray-triton-models:latest- La configuración actual usa CPU only para compatibilidad con Kind local
- Para usar GPU en producción:
- Cambia
num_cpus: 1anum_gpus: 1enserve.pyyconfig-serve-docker.yaml - Añade recursos GPU en el spec del worker
- Asegúrate de que los nodos de Kubernetes tengan GPU disponibles
- Cambia
- Ray version: 2.46.0
- Triton Server version: 24.03