AulaML Python · C++
C++ avanzado · 62 min · Proyectos reales

Proyecto: servidor de inferencia

Cargar pesos, servir predicciones por HTTP, medir la concurrencia y desplegar en Docker

Un modelo entrenado en Python no sirve de nada si nadie puede llamarlo. Construirás un servicio en C++ que carga los pesos desde JSON, valida la entrada, responde por HTTP en milisegundos, atiende peticiones concurrentes y se despliega en una imagen Docker mínima, con mediciones y límites explicados.

Lección 2 de 2 del módulo
Al terminar esta lección podrás
  • Cargar pesos y metadatos de un modelo desde JSON con validación explícita
  • Implementar la inferencia de una regresión logística sin dependencias externas
  • Exponer endpoints /health y /predict con códigos de estado correctos
  • Analizar la concurrencia de un servidor con pool de hilos y medir latencia y throughput
  • Construir y ejecutar una imagen Docker de dos etapas

Antes necesitas: Proyecto: motor matricial · Excepciones y std::thread · Archivos, JSON y CSV

En esta lección
  1. ¿Qué es un servidor de inferencia?
  2. ¿Para qué sirve?
  3. Cómo funciona por dentro
  4. Estructura y dependencias
  5. Sintaxis: el modelo como contrato de datos
  6. Ejemplo básico: cargar pesos y predecir sin red
  7. Ejemplo intermedio: el servidor HTTP
  8. Visualización: de features a probabilidad
  9. Visualización: una petición paso a paso
  10. Caso práctico: concurrencia, carga y Docker
  11. Errores comunes
  12. Buenas prácticas
  13. Ejercicios
  14. Soluciones explicadas
  15. Resumen
  16. Evaluación

Entrenaste el modelo en Python, guardaste los coeficientes y ahora la aplicación necesita una respuesta en pocos milisegundos por petición. Arrancar un intérprete en cada llamada cuesta cientos de milisegundos; dejar un notebook abierto no es un servicio. La pieza que falta es un proceso que permanezca vivo, tenga el modelo en memoria y hable HTTP.

Ese proceso es un servidor de inferencia, y aquí lo construyes completo: carga de pesos, validación, concurrencia, imagen Docker y una medición honesta de lo que aguanta.

¿Qué es un servidor de inferencia?

Es un programa de larga vida que expone un modelo por la red: recibe datos, aplica el modelo entrenado y devuelve una predicción. No entrena, no guarda historial por petición y no sabe quién llama. Es una función pura con interfaz de red, y esa pureza permite atender a muchos clientes a la vez.

Piensa en un mostrador de comida: toma el pedido, la cocina lo prepara y el plato vuelve. Si la cocina se bloquea, la cola crece y el cliente percibe esperas de minutos aunque cada plato tarde lo mismo. Un servidor se juzga por esa cola, no por la operación aislada.

¿Para qué sirve?

  • Servir una predicción a una aplicación: el frontend envía features, recibe probabilidad y decide qué mostrar.
  • Descargar trabajo de un servicio Python: mover solo el cálculo pesado a C++ y dejar la orquestación donde ya está.
  • Evaluar A/B de versiones: dos contenedores con modelos distintos y un proxy que reparte tráfico.
  • Aprender los límites reales: latencia, concurrencia y memoria dejan de ser teoría cuando ves la cola crecer.

En un sistema de Machine Learning esto ocupa el último tramo: el training pipeline produce pesos, el registro los versiona y el servidor los carga. El contrato entre ambos es el archivo JSON con su versión, no el código. Si el modelo cambia, la interfaz HTTP puede seguir igual.

Cómo funciona por dentro

Cada petición recorre la misma cadena: aceptar la conexión, parsear HTTP, parsear el JSON, validar el campo esperado, convertir a std::vector<double>, multiplicar por los pesos, aplicar la sigmoide, serializar y escribir en el socket. Con cuatro features el cálculo es ruido: el coste está en el resto.

EtapaOrden de magnitudQué la domina
Conexión TCP y HTTPdecenas de µsSyscalls, latencia de red local
Parseo del JSON1–5 µsTamaño del cuerpo, asignaciones
Validación y conversión< 1 µsComprobaciones y copia a vector
Producto escalar y sigmoidedecenas de nsCaché L1, una exponencial
Serialización de salida< 1 µsFormateo de un double

La concurrencia decide cuántas cadenas avanzan a la vez. cpp-httplib usa un pool de hilos: cada conexión se atiende en un hilo del pool, que por defecto mide max(8, núcleos - 1). Un manejador que duerme bloquea su hilo, y con keep-alive una conexión inactiva también lo ocupa. Por eso el tamaño del pool y la concurrencia del cliente deben conversar.

El modelo cargado nunca cambia: es de solo lectura y varios hilos pueden llamarlo sin candados. Lo único mutable es el contador de peticiones, que basta con un std::atomic. La recarga en caliente rompe esa inmutabilidad y exige sincronización explícita.

Estructura y dependencias

SHELL árbol del proyecto
servidor/
├── CMakeLists.txt
├── Dockerfile
├── include/
│   ├── httplib.h          # biblioteca HTTP (una sola cabecera)
│   ├── nlohmann/
│   │   └── json.hpp       # JSON (una sola cabecera)
│   └── model.hpp
├── models/
│   └── logistic.json      # pesos y metadatos del modelo
├── src/
│   ├── model.cpp          # carga y prediccion
│   ├── predecir.cpp       # cliente de consola, sin red
│   └── main.cpp           # servidor HTTP
└── payload.json           # cuerpo de prueba para ab

Ambas dependencias son de cabecera: cpp-httplib para el servidor y nlohmann/json para el parseo. Puedes dejarlas dentro del repositorio, de modo que compilar no necesite red y la versión quede fijada. FetchContent en CMake las descarga al configurar: cómodo, pero cada build depende de la red.

SHELL instalación de las cabeceras (versiones fijadas)
mkdir -p include/nlohmann

curl -L -o include/httplib.h \
  https://raw.githubusercontent.com/yhirose/cpp-httplib/v0.16.3/httplib.h

curl -L -o include/nlohmann/json.hpp \
  https://raw.githubusercontent.com/nlohmann/json/v3.11.3/single_include/nlohmann/json.hpp

Sintaxis: el modelo como contrato de datos

JSON models/logistic.json
{
  "version": "1.0.0",
  "features": ["edad", "ingreso_mensual", "visitas", "deuda"],
  "bias": -1.2,
  "weights": [0.03, 0.00012, 0.4, -0.002]
}

El archivo dice todo lo necesario: versión, orden exacto de las features, sesgo y pesos. Ese orden es parte del contrato; si el cliente envía los valores en otro, la predicción es silenciosamente incorrecta. De ahí que cada respuesta incluya la versión del modelo y que convenga validar el esquema al arrancar.

CPP include/model.hpp
#pragma once

#include <cstddef>
#include <string>
#include <vector>

struct Prediccion {
    double probabilidad;
    int etiqueta;
};

// Regresion logistica con pesos cargados desde JSON.
// Es inmutable tras cargar: se puede usar desde varios hilos sin sincronizar.
class ModeloLogistico {
public:
    static ModeloLogistico cargar(const std::string& ruta);

    std::size_t num_features() const noexcept { return pesos_.size(); }
    const std::string& version() const noexcept { return version_; }
    const std::vector<std::string>& features() const noexcept { return features_; }

    Prediccion predecir(const std::vector<double>& x) const;

private:
    std::string version_;
    std::vector<std::string> features_;
    std::vector<double> pesos_;
    double sesgo_ = 0.0;
};

La clase no expone setters: se construye con cargar y solo se consulta. Esa inmutabilidad hace segura la concurrencia. cargar es estática porque un objeto a medio construir no debería existir: o el archivo es válido y devuelve un modelo completo, o lanza una excepción.

Ejemplo básico: cargar pesos y predecir sin red

CPP src/model.cpp
#include "model.hpp"

#include <cmath>
#include <fstream>
#include <nlohmann/json.hpp>
#include <stdexcept>

ModeloLogistico ModeloLogistico::cargar(const std::string& ruta) {
    std::ifstream entrada(ruta);
    if (!entrada) {
        throw std::runtime_error("no se pudo abrir el modelo: " + ruta);
    }

    nlohmann::json documento;
    try {
        documento = nlohmann::json::parse(entrada);
    } catch (const nlohmann::json::parse_error& error) {
        throw std::runtime_error(std::string("JSON invalido en el modelo: ") + error.what());
    }

    ModeloLogistico modelo;
    try {
        modelo.version_ = documento.at("version").get<std::string>();
        modelo.features_ = documento.at("features").get<std::vector<std::string>>();
        modelo.pesos_ = documento.at("weights").get<std::vector<double>>();
        modelo.sesgo_ = documento.at("bias").get<double>();
    } catch (const nlohmann::json::exception& error) {
        throw std::runtime_error(std::string("modelo incompleto: ") + error.what());
    }

    if (modelo.pesos_.size() != modelo.features_.size()) {
        throw std::runtime_error("el modelo tiene " + std::to_string(modelo.pesos_.size()) +
                                 " pesos y " + std::to_string(modelo.features_.size()) + " features");
    }
    return modelo;
}

Prediccion ModeloLogistico::predecir(const std::vector<double>& x) const {
    if (x.size() != pesos_.size()) {
        throw std::invalid_argument("se esperaban " + std::to_string(pesos_.size()) +
                                    " features y llegaron " + std::to_string(x.size()));
    }

    double z = sesgo_;
    for (std::size_t i = 0; i < pesos_.size(); ++i) {
        z += pesos_[i] * x[i];
    }

    const double probabilidad = 1.0 / (1.0 + std::exp(-z));
    return {probabilidad, probabilidad >= 0.5 ? 1 : 0};
}
CPP src/predecir.cpp
#include <exception>
#include <iostream>
#include <vector>

#include "model.hpp"

int main() {
    try {
        const ModeloLogistico modelo = ModeloLogistico::cargar("models/logistic.json");
        const std::vector<double> x{34.0, 2500.0, 4.0, 1200.0};
        const Prediccion p = modelo.predecir(x);

        std::cout << "modelo " << modelo.version() << " con " << modelo.num_features() << " features\n";
        std::cout << "probabilidad = " << p.probabilidad << "\n";
        std::cout << "etiqueta = " << p.etiqueta << "\n";
    } catch (const std::exception& error) {
        std::cerr << "error: " << error.what() << "\n";
        return 1;
    }
    return 0;
}
SHELL compilar y ejecutar
g++ -O2 -std=c++20 -Wall -Wextra -Iinclude \
    src/model.cpp src/predecir.cpp -o predecir

./predecir

# modelo 1.0.0 con 4 features
# probabilidad = 0.336261
# etiqueta = 0

Comprueba la cuenta: z = -1.2 + 0.03·34 + 0.00012·2500 + 0.4·4 − 0.002·1200 = −0.68, y 1 / (1 + e^0.68) = 0.336. Si no coincide, el problema está en el orden de los pesos o en una feature mal escalada. Cambia la edad a 52 y la probabilidad salta por encima de 0.99: los pesos están pensados para esas magnitudes.

Separar la inferencia de la red tiene una ventaja práctica: este programa se depura sin clientes y sirve de prueba de humo del modelo. Cuando algo falla en producción, ejecutarlo con el mismo logistic.json descarta medio problema.

Ejemplo intermedio: el servidor HTTP

CPP src/main.cpp
#include <atomic>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <string>
#include <vector>

#include <nlohmann/json.hpp>

#include "httplib.h"
#include "model.hpp"

namespace {

using nlohmann::json;

std::string variable_entorno(const char* nombre, const std::string& por_defecto) {
    const char* valor = std::getenv(nombre);
    return valor != nullptr ? std::string(valor) : por_defecto;
}

void responder_error(httplib::Response& res, int codigo, const std::string& mensaje) {
    res.status = codigo;
    res.set_content(json{{"error", mensaje}}.dump(), "application/json");
}

}  // namespace

int main() {
    const std::string ruta_modelo = variable_entorno("MODELO_PATH", "models/logistic.json");
    const int puerto = std::stoi(variable_entorno("PORT", "8080"));

    ModeloLogistico modelo;
    try {
        modelo = ModeloLogistico::cargar(ruta_modelo);
    } catch (const std::exception& error) {
        std::cerr << "no se pudo cargar el modelo: " << error.what() << "\n";
        return 1;
    }

    std::atomic<long> peticiones{0};
    httplib::Server servidor;

    servidor.set_payload_max_length(64 * 1024);
    servidor.set_read_timeout(5, 0);
    servidor.set_write_timeout(5, 0);

    servidor.Get("/health", [&modelo](const httplib::Request&, httplib::Response& res) {
        res.set_content(json{{"estado", "ok"},
                             {"modelo", modelo.version()},
                             {"features", modelo.num_features()}}
                            .dump(),
                        "application/json");
    });

    servidor.Get("/stats", [&peticiones](const httplib::Request&, httplib::Response& res) {
        res.set_content(json{{"peticiones", peticiones.load()}}.dump(), "application/json");
    });

    servidor.Post("/predict",
                  [&modelo, &peticiones](const httplib::Request& req, httplib::Response& res) {
                      ++peticiones;

                      json cuerpo;
                      try {
                          cuerpo = json::parse(req.body);
                      } catch (const json::parse_error&) {
                          responder_error(res, 400, "el cuerpo no es JSON valido");
                          return;
                      }

                      if (!cuerpo.contains("features") || !cuerpo["features"].is_array()) {
                          responder_error(res, 400, "falta el array 'features'");
                          return;
                      }

                      std::vector<double> x;
                      try {
                          x = cuerpo["features"].get<std::vector<double>>();
                      } catch (const json::type_error&) {
                          responder_error(res, 400, "'features' debe contener solo numeros");
                          return;
                      }

                      try {
                          const Prediccion p = modelo.predecir(x);
                          res.set_content(json{{"probabilidad", p.probabilidad},
                                               {"etiqueta", p.etiqueta},
                                               {"modelo", modelo.version()}}
                                              .dump(),
                                          "application/json");
                      } catch (const std::exception& error) {
                          responder_error(res, 400, error.what());
                      }
                  });

    std::cout << "escuchando en 0.0.0.0:" << puerto << " con modelo " << modelo.version() << "\n";
    if (!servidor.listen("0.0.0.0", puerto)) {
        std::cerr << "no se pudo abrir el puerto " << puerto << "\n";
        return 1;
    }
    return 0;
}
SHELL compilar, arrancar y probar
g++ -O2 -std=c++20 -Wall -Wextra -Iinclude -pthread \
    -DCPPHTTPLIB_THREAD_POOL_COUNT=64 \
    -DCPPHTTPLIB_LISTEN_BACKLOG=128 \
    src/model.cpp src/main.cpp -o servidor

./servidor &
# escuchando en 0.0.0.0:8080 con modelo 1.0.0

curl -s localhost:8080/health
# {"estado":"ok","features":4,"modelo":"1.0.0"}

curl -s -X POST localhost:8080/predict \
     -H 'Content-Type: application/json' \
     -d '{"features":[34,2500,4,1200]}'
# {"etiqueta":0,"modelo":"1.0.0","probabilidad":0.33626130259564735}

curl -s -X POST localhost:8080/predict -d '{"x":1}'
# {"error":"falta el array 'features'"}

curl -s -X POST localhost:8080/predict -d '{"features":[1,2,3]}'
# {"error":"se esperaban 4 features y llegaron 3"}

Cada rechazo devuelve 400 con un mensaje que dice qué falta, y ningún error se convierte en un 200 con un campo error dentro: el código de estado es el contrato que entienden clientes y balanceadores. El 500 se reserva para fallos del servidor; aquí casi todos los fallos posibles son del cliente, porque el modelo ya se validó al arrancar.

Fíjate en las dos definiciones de compilación. CPPHTTPLIB_LISTEN_BACKLOG fija cuántas conexiones esperan en la cola del sistema; su valor por defecto es 5, y con 50 clientes eso produce rechazos. CPPHTTPLIB_THREAD_POOL_COUNT fija el pool, por defecto max(8, núcleos - 1).

Visualización: de features a probabilidad

La predicción es una suma ponderada. Recorre las celdas: cada una es el peso multiplicado por su feature, y el sesgo se suma aparte. Si un peso tiene el signo cambiado, la contribución cambia de lado y la probabilidad se mueve en la dirección contraria.

Mapa de datos ·Contribución de cada feature a z (pesos · entrada)
1 × 4 rango [-2.4, 1.6] edad: 0.03 · 34 = 1.02. Es la única feature que sube con la edad en este modelo.
edad
ingreso
visitas
deuda
w · x
1.02
0.3
1.6
-2.4

Recorre los pasos para ver cómo cambian las celdas.

Visualización: una petición paso a paso

Ahora el mismo cálculo dentro del manejador. Observa que el cuerpo llega como texto, se convierte en un objeto JSON, después en un vector de double y solo entonces entra en el modelo. Cada conversión puede fallar, y cada fallo tiene su respuesta.

Ejecución visual ·Recorrido de POST /predict
1 / 7
1 servidor.Post("/predict", [&](const httplib::Request& req, httplib::Response& res) {
2 json cuerpo = json::parse(req.body);
3 std::vector<double> x = cuerpo["features"].get<std::vector<double>>();
4 Prediccion p = modelo.predecir(x);
5 res.set_content(json{{"probabilidad", p.probabilidad}}.dump(), "application/json");
6 });
Llega la petición con Content-Type: application/json y 40 bytes de cuerpo. httplib ya parseó la línea de petición y las cabeceras.
Variables
req.body = {"features":[34,2500,4,1200]}
cuerpo = —
x = —
p = —
Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Caso práctico: concurrencia, carga y Docker

La pregunta correcta no es «¿cuántas peticiones por segundo hace?» sino «¿qué pasa con el percentil 99 cuando hay 50 clientes?». Con el pool por defecto y keep-alive, las conexiones inactivas ocupan hilos y las peticiones esperan hasta agotar el tiempo de lectura. Es un efecto medible que se arregla con una línea.

Configuración del servidorPeticiones/sp99
Pool por defecto max(8, núcleos-1), backlog 1281995012 ms
THREAD_POOL_COUNT=64, backlog 128414247 ms
SHELL medir con ApacheBench (cuerpo en payload.json)
printf '{"features":[34,2500,4,1200]}' > payload.json

ab -n 3000 -c 50 -k -p payload.json -T application/json \
   http://127.0.0.1:8080/predict

# Complete requests:      3000
# Failed requests:        0
# Requests per second:    41424.45 [#/sec] (mean)
# Time per request:       1.207 [ms] (mean)
#  50%      1
#  99%      7

La tabla se midió en un Apple Silicon con ab -n 3000 -c 50 -k; tus cifras cambiarán, pero el salto se repite siempre que el pool sea menor que la concurrencia y el keep-alive esté activo. La lección: el throughput de un servidor bloqueante lo acota el número de hilos, no la velocidad del modelo. ab es monohilo y puede convertirse en el cuello; para cifras serias usa wrk o hey y vigila la CPU del cliente.

Docker cierra el círculo con dos etapas: la primera instala compilador y CMake y produce el binario; la segunda copia solo el ejecutable y sus librerías. El compilador no viaja a producción y la imagen final baja de cientos a decenas de megabytes.

SHELL Dockerfile
# ---- etapa 1: compilacion ----
FROM debian:bookworm-slim AS build
RUN apt-get update \
    && apt-get install -y --no-install-recommends g++ cmake make \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY include/ include/
COPY src/ src/
COPY models/ models/
COPY CMakeLists.txt .
RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
    && cmake --build build -j"$(nproc)"

# ---- etapa 2: imagen final, sin compilador ----
FROM debian:bookworm-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends libstdc++6 curl \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build /app/build/servidor /usr/local/bin/servidor
COPY models/ models/
ENV MODELO_PATH=/app/models/logistic.json
ENV PORT=8080
EXPOSE 8080
USER nobody
ENTRYPOINT ["/usr/local/bin/servidor"]
SHELL construir, ejecutar y comprobar
docker build -t inferencia:0.1 .

docker run --rm -d --name inferencia -p 8080:8080 inferencia:0.1

curl -s localhost:8080/health
# {"estado":"ok","features":4,"modelo":"1.0.0"}

docker logs inferencia
# escuchando en 0.0.0.0:8080 con modelo 1.0.0

docker stop inferencia

Errores comunes

El código de estado es parte del contrato
Incorrecto
PYTHON Incorrecto: todo responde 200
res.set_content(json{{"error", "faltan features"}}.dump(), "application/json");
// el cliente recibe 200 y tiene que adivinar si hubo exito
Correcto
PYTHON Correcto: 400 para el cliente, 500 para el servidor
res.status = 400;
res.set_content(json{{"error", "falta el array 'features'"}}.dump(), "application/json");

Un cliente que solo mira el cuerpo no puede distinguir éxito de error, y un balanceador tampoco puede contar fallos. Los códigos 4xx indican que la petición está mal; los 5xx, que el servidor falló. Mezclarlos rompe reintentos, alertas y paneles de disponibilidad.

  • Deserializar sin comprobar el campo: cuerpo["features"] sobre un JSON sin esa clave lanza type_error dentro del manejador y produce un 500 en lugar de un 400 explicativo.
  • Cambiar el orden de las features: el modelo no lo detecta. Fija el orden en el JSON, expón los nombres en /health y valida la longitud; un desajuste de semántica es el error más caro de diagnosticar.
  • Suponer que el modelo cabe en cualquier formato: pickle ata el archivo a una versión de Python. JSON o un binario con cabecera son portables; elige uno y documenta el esquema.
  • Recargar el modelo sin candado: asignar modelo_ = nuevo mientras otro hilo predice es una carrera. Carga fuera del candado y sustituye dentro de un unique_lock.
  • Ignorar el pool y el backlog: con los valores por defecto, 50 clientes concurrentes producen conexiones rechazadas y esperas de segundos. Ajusta ambos o limita la concurrencia en el proxy.
  • Registrar el cuerpo completo: las features suelen ser datos personales. Registra estado, latencia y versión, no los valores.

Buenas prácticas

  • Valida el modelo al arrancar y falla rápido: un servicio que arranca sin modelo es peor que uno que no arranca.
  • Devuelve la versión del modelo en cada respuesta; sin ella no puedes correlacionar un cambio de comportamiento con un despliegue.
  • Mantén el modelo inmutable después de cargarlo y protege cualquier recarga con un candado de lectura/escritura.
  • Fija las versiones de las dependencias y guárdalas en el repositorio; una cabecera que cambia sola es un incidente esperando.
  • Mide latencia por percentiles, no por media: la media esconde exactamente la cola que sufren los usuarios.
  • Usa una imagen de dos etapas y ejecuta como usuario sin privilegios; el compilador no tiene nada que hacer en producción.
  • Documenta el contrato con un ejemplo de petición y otro de error; los clientes se escriben a partir de ejemplos.
La misma predicción antes y después de servirla
Python
PYTHON predecir.py
import json
import math

with open("models/logistic.json", encoding="utf-8") as f:
    modelo = json.load(f)

x = [34, 2500, 4, 1200]
z = modelo["bias"] + sum(w * v for w, v in zip(modelo["weights"], x))
print(1 / (1 + math.exp(-z)))   # 0.33626130259564735
C++
CPP predecir.cpp
double z = sesgo_;
for (std::size_t i = 0; i < pesos_.size(); ++i) z += pesos_[i] * x[i];
const double p = 1.0 / (1.0 + std::exp(-z));
return {p, p >= 0.5 ? 1 : 0};

El resultado debe ser idéntico bit a bit o casi: ambos usan `double` de 64 bits y la misma exponencial de la libm. Si difieren en el séptimo decimal, suele ser por el orden de la suma. La diferencia real no está en la aritmética sino en el contexto: Python carga el JSON en cada ejecución, C++ lo hace una vez y sirve miles de peticiones.

Ejercicios

Ejercicio 1

Predicción por lotes

Escribe predecir_lote para aplicar el modelo a cada fila de un lote, reservando el vector de salida. Pruébalo con [[34, 2500, 4, 1200], [52, 7000, 15, 300]] y comprueba que las etiquetas son 0 y 1. Después conecta un endpoint /predict/batch que reciba {"lote": [[...], [...]]}.
Pista

reserve evita reallocaciones. Para el endpoint, valida que lote sea un array de arrays y reutiliza el mismo manejo de errores que ya tienes.

Ejercicio 2

Recarga en caliente sin carreras

Envuelve el modelo en ModeloServible con un std::shared_mutex: predecir toma el candado compartido y recargar el exclusivo, cargando el archivo nuevo fuera del candado. Comprueba que recargar y predecir intercalados dan siempre el mismo valor.
Pista

std::shared_lock para lectores, std::unique_lock para el escritor. Cargar dentro del candado bloquearía todas las predicciones durante la lectura de disco.

Completar código Completa la sigmoide
#include <cmath>

double probabilidad(double z) {
    return 1.0 / (1.0 + std::());
}

La sigmoide es 1 / (1 + e^(-z)). La función de C++ para e^x es std::exp.

Soluciones explicadas

El lote y la petición individual son el mismo cálculo con distinto empaquetado. El coste fijo por petición —conexión, cabeceras, parseo— se paga una vez, y por eso los servicios reales aceptan lotes: un modelo pequeño servido de uno en uno gasta casi todo el tiempo en burocracia. La contrapartida es la latencia: agrupar introduce una espera artificial.

La recarga en caliente es un problema de concurrencia clásico: dos hilos, un recurso compartido, un lector lento y un escritor. El candado funciona porque las lecturas son frecuentes y la escritura rara; si la recarga fuera constante, publicar un std::shared_ptr<const ModeloLogistico> con carga atómica sería mejor, porque cada petición copiaría el puntero sin bloquear a nadie.

Resumen

  • Un servidor de inferencia es una función pura expuesta por red: recibe features, devuelve predicción, no guarda estado.
  • Separa carga, validación e inferencia; cada etapa tiene su propio modo de fallo y su propio mensaje.
  • Carga el modelo al arrancar y falla rápido; expón su versión en cada respuesta.
  • Los códigos de estado son el contrato: 400 para entradas inválidas, 500 para fallos del servidor.
  • cpp-httplib usa un pool de hilos; su tamaño y el backlog del socket limitan el throughput real.
  • Un modelo inmutable se comparte sin candados; la recarga en caliente exige shared_mutex o punteros atómicos.
  • Mide p50 y p99 bajo carga; la media oculta la cola que sufre el usuario.
  • Docker en dos etapas deja el compilador fuera de producción y fija el entorno de ejecución.

Evaluación

Evaluación 1

El cliente envía {"features": [34, 2500]} a un modelo de 4 features. ¿Qué debe responder el servidor?

Evaluación 2

¿Por qué el modelo se declara inmutable después de cargarlo?

Evaluación 3

Con el pool por defecto, 50 clientes con keep-alive provocan esperas de segundos. ¿Cuál es la causa más probable?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.