El servei de churn està desplegat amb 3 rèpliques i autoescalat. La pregunta següent la farà finances abans que enginyeria: quant costa això i podria costar menys? I producte hi afegirà la seva: de debò responem sempre dins dels 300 ms? L'optimització d'inferència respon a totes dues, però té una regla d'or que aquesta lliçó repetirà fins a l'avorriment: mesurar abans d'optimitzar. Optimitzar sense mesurar és endevinar, i en inferència les intuïcions fallen estrepitosament (spoiler: el predict gairebé mai no és on se'n va el temps, i la GPU gairebé mai no és la resposta per a un model tabular). En aquesta lliçó mesurarem el servei de CineClick amb percentils — no amb mitjanes —, desglossarem on se'n va cada mil·lisegon, i aplicarem les optimitzacions per ordre de rendibilitat: vectorització, memòria cau, model més lleuger, ONNX i (des)cartar maquinari. Tancarem el mòdul fent balanç del que hem construït... i de tot el que encara fem a mà.

Contingut

  1. Mesurar abans d'optimitzar: percentils, throughput i cost
  2. Mesurar el servei de CineClick: un script de càrrega
  3. On se'n va el temps en una predicció
  4. Optimització 1: predicció vectoritzada (batch de peticions)
  5. Optimització 2: cacejar features i respostes
  6. Optimització 3: un model més lleuger, amb la pèrdua quantificada
  7. Optimització 4: ONNX, un format pensat per a inferència
  8. Optimització 5 (o no): CPU davant GPU
  9. La taula de palanques i el tancament del mòdul

Mesurar abans d'optimitzar: percentils, throughput i cost

Tres mètriques defineixen el rendiment d'un servei d'inferència:

Latència per percentils. La mitjana enganya, i convé entendre per què amb un exemple: si 99 peticions triguen 20 ms i una en triga 2.000, la mitjana és ~40 ms — "tot bé" — mentre un client de cada cent pateix 2 segons mirant la pantalla de cancel·lació. Per això es mesura amb percentils:

  • p50 (mediana): l'experiència típica. La meitat de les peticions responen més ràpid.
  • p95: l'experiència dels pitjors 1 de cada 20. Aquí viuen els efectes de pics, GC, contenció.
  • p99: la cua llarga. En serveis de cara a l'usuari, el p99 és el que trenca els pressupostos — i el primer que empitjora quan alguna cosa va malament.

L'objectiu de CineClick s'expressa així: p95 < 300 ms (no "mitjana de 300 ms": això permetria cues horribles).

Throughput: peticions per segon que el servei sosté sense degradar la latència. Latència i throughput es compren mútuament: en acostar-te a la saturació, el p99 es dispara molt abans que el servei "caigui".

Cost per 1.000 prediccions: la mètrica que uneix enginyeria amb finances. Es calcula des del cost d'infraestructura i el volum real:

cost_per_1000 = (cost_mensual_infra / prediccions_al_mes) × 1000

Amb números de CineClick: 3 rèpliques amb request de 0.25 CPU / 512 MiB en un Kubernetes gestionat costen de l'ordre de 90 €/mes; amb ~600.000 prediccions/mes del flux de cancel·lació, surt a 0,15 € per 1.000 prediccions. Aquest número converteix qualsevol optimització en una frase que finances entén: "la memòria cau redueix el cost per mil a la meitat".

Mesurar el servei de CineClick: un script de càrrega

No necessites una eina pesant per a una primera mesura honesta. Un script amb httpx i asyncio que llanci peticions concurrents i calculi percentils:

# scripts/mesurar_carrega.py — mesura de latència amb percentils
import asyncio
import statistics
import time

import httpx

URL = "http://localhost:8000/predir"
CLIENT_PROVA = {
    "id_client": "CLI-04217", "antiguitat_mesos": 3, "hores_setmana": 1.5,
    "tiquets_suport": 4, "pla": "basic", "metode_pagament": "targeta",
    "descompte_actiu": False,
}
N_PETICIONS = 1000
CONCURRENCIA = 20   # peticions simultànies: simula trànsit real, no una cua d'un en un

async def una_peticio(client: httpx.AsyncClient, sem: asyncio.Semaphore) -> float:
    async with sem:                       # limita la concurrència al valor fixat
        t0 = time.perf_counter()
        r = await client.post(URL, json=CLIENT_PROVA)
        r.raise_for_status()
        return (time.perf_counter() - t0) * 1000   # ms

async def main() -> None:
    sem = asyncio.Semaphore(CONCURRENCIA)
    async with httpx.AsyncClient(timeout=10) as client:
        t0 = time.perf_counter()
        latencies = await asyncio.gather(
            *[una_peticio(client, sem) for _ in range(N_PETICIONS)]
        )
        durada = time.perf_counter() - t0

    latencies.sort()
    def pct(p: float) -> float:
        return latencies[int(len(latencies) * p) - 1]

    print(f"peticions: {N_PETICIONS}  concurrencia: {CONCURRENCIA}")
    print(f"throughput: {N_PETICIONS / durada:.0f} req/s")
    print(f"p50: {pct(0.50):.1f} ms   p95: {pct(0.95):.1f} ms   p99: {pct(0.99):.1f} ms")
    print(f"mitjana (per comparar): {statistics.mean(latencies):.1f} ms")

if __name__ == "__main__":
    asyncio.run(main())

Resultat contra el contenidor local de 04-03 (portàtil de desenvolupament; els números absoluts variaran a la teva màquina — el que importa són les proporcions):

peticions: 1000  concurrencia: 20
throughput: 210 req/s
p50: 38.2 ms   p95: 92.5 ms   p99: 148.7 ms
mitjana (per comparar): 47.1 ms

Lectura: p95 de ~93 ms — dins del pressupost de 300 ms amb marge. Fixa't que la mitjana (47 ms) és un 23% més gran que la p50: la cua ja treu el nas. Per a càrregues més serioses hi ha eines dedicades — locust (Python, escenaris programables) i k6 (JavaScript, molt usat en plataformes) — que afegeixen rampes de càrrega, usuaris virtuals i informes; no les cobrim aquí, però el concepte és el mateix: concurrència controlada + percentils. I una distinció d'abast: això és una mesura puntual de rendiment, una foto; la vigilància contínua d'aquestes mateixes mètriques en producció és monitoratge, i arriba a la lliçó 06-01.

On se'n va el temps en una predicció

Abans d'optimitzar res, desglossem una petició. Instrumentem cada tram de l'endpoint amb time.perf_counter() (temporalment, o millor: deixant els temps per tram al log estructurat de 04-02) i fem la mitjana sobre mil peticions:

Tram Què fa Temps típic % del total
Deserialització + validació JSON → objecte Pydantic validat ~1 ms 3%
Construcció del DataFrame dict → pd.DataFrame d'1 fila ~8 ms 21%
construir_features ratio_tickets, encodings... sobre 1 fila ~9 ms 24%
predict_proba els 300 arbres del RandomForest ~18 ms 47%
Serialització de resposta objecte → JSON ~1 ms 3%
Xarxa i framework HTTP, routing, threadpool ~1 ms 3%

Dues sorpreses que gairebé tothom s'emporta la primera vegada:

  • Més de la meitat del temps NO és el model. Crear un DataFrame d'una fila i passar features per pandas costa gairebé tant com els 300 arbres: pandas està dissenyat per operar sobre milers de files, i el seu sobrecost fix per operació es menja les files solitàries.
  • El predict_proba d'1 fila triga ~18 ms... i el de 1.000 files triga ~50 ms. No mil vegades més: tres vegades més. Aquest abisme és la primera optimització.

Moralitat: si haguéssim "optimitzat el model" sense mesurar, hauríem atacat el 47% ignorant el 45% que se'n va a preparar una fila. Els perfils manen.

Optimització 1: predicció vectoritzada (batch de peticions)

La demostració amb números, executable tal qual:

# scripts/demo_vectoritzacio.py
import time
import mlflow
import pandas as pd
from cineclick_churn.features import construir_features

model = mlflow.sklearn.load_model("models:/churn-cineclick@champion")
df = pd.read_csv("data/clients_churn.csv").drop(columns=["id_client", "abandonament"])
X = construir_features(df).head(1000)

# A) 1000 prediccions d'1 fila (el que fa l'API amb peticions soltes)
t0 = time.perf_counter()
for i in range(1000):
    model.predict_proba(X.iloc[[i]])
print(f"1000 x predict(1 fila): {time.perf_counter() - t0:.2f} s")

# B) 1 predicció de 1000 files
t0 = time.perf_counter()
model.predict_proba(X)
print(f"1 x predict(1000 files): {time.perf_counter() - t0:.2f} s")
1000 x predict(1 fila): 18.4 s
1 x predict(1000 files): 0.05 s

Un factor ~370x. La raó: cada crida a predict_proba paga un cost fix (validació d'entrada, conversió a arrays, despatx als arbres) que domina quan la càrrega útil és una fila; sobre mil files, aquest cost s'amortitza i els arbres processen en vectoritzat. Com explotar-ho a CineClick:

  • El batch setmanal ja ho fa bé: l'score_batch.py de 04-01 puntua tota la base en un sol predict_proba. Puntuar 100.000 clients costa segons — mai no escriguis aquest job com un bucle de crides a l'API (100.000 × ~40 ms ≈ més d'una hora, més el cost de tenir l'API aguantant-ho).
  • L'endpoint /predir-lot (exercici de 04-02) dona als consumidors interns amb N clients la via vectoritzada: una crida, un predict_proba.
  • Per al flux de cancel·lació (peticions d'1 fila que arriben soltes) existeix una tècnica avançada, el micro-batching adaptatiu — acumular les peticions d'una finestra de pocs ms i predir-les juntes —, que els frameworks de serving especialitzats porten de sèrie. Amb el nostre p95 de 93 ms no la necessitem; amb conèixer-la n'hi ha prou.

Optimització 2: cacejar features i respostes

Segona palanca: no calcular dues vegades el mateix. És el patró híbrid de 04-01 (precomputar + servir de memòria cau) aplicat en petit, dins del servei:

  • Cacejar la resposta completa per hash de l'input (ja calculem aquest hash per al log a 04-02!): si el mateix client amb les mateixes dades pregunta dues vegades en poc temps — reintents del frontend, doble clic, l'usuari que va i torna de la pantalla de cancel·lació — la segona resposta surt de memòria en <1 ms.
  • TTL curt i deliberat: la memòria cau caduca (per exemple, als 300 segons) perquè la frescor era la raó de ser del patró online. Un TTL de 5 minuts és un compromís explícit: "acceptem servir una predicció de fa ≤5 min a canvi de retallar cost"; un TTL d'hores ens retornaria, per la porta del darrere, al problema del batch envellit.
# esquema de memòria cau en memòria amb TTL (per rèplica; per compartir entre rèpliques: Redis)
import time

CACHE: dict[str, tuple[float, "PrediccioSortida"]] = {}
TTL_SEGONS = 300

def obtenir_cachejada(clau: str):
    entrada = CACHE.get(clau)
    if entrada and time.time() - entrada[0] < TTL_SEGONS:
        return entrada[1]          # encert: resposta en microsegons
    return None

def desar(clau: str, sortida) -> None:
    CACHE[clau] = (time.time(), sortida)

La clau és el hash_entrada(payload) de 04-02. Amb una taxa d'encerts del 20% (mesurable: registra encerts/fallades al log), el 20% de les peticions passa de ~40 ms a <1 ms i — més rellevant per a finances — deixa de consumir CPU: menys rèpliques de l'HPA en pic. Si el tram car fossin les features (agregacions sobre històric, crides a altres serveis), es cacejarien les features per client en lloc de la resposta; a CineClick construir_features és barata i la resposta completa és la unitat natural.

Optimització 3: un model més lleuger, amb la pèrdua quantificada

Els 300 arbres amb max_depth=20 del campió, són tots necessaris en serving? La Laura reentrena variants reduïdes (mateixes dades, mateix pipeline dvc repro, tot traçat a l'experiment churn-cineclick de MLflow, com mana el mòdul 3) i mesurem les tres dimensions que importen:

Variant Mida en disc p95 latència (1 fila) Recall Precision F1
Campió v2 (300 arbres, prof. 20) 145 MB 92 ms 0.68 0.55 0.61
150 arbres, prof. 20 74 MB 71 ms 0.67 0.55 0.60
100 arbres, prof. 12 21 MB 58 ms 0.65 0.54 0.59
50 arbres, prof. 8 4 MB 49 ms 0.60 0.52 0.56

(Mètriques sobre el mateix conjunt d'avaluació del mòdul 3; latències de l'script de càrrega, mateixa màquina.)

La lectura correcta d'aquesta taula no és tècnica sinó de negoci: passar de 300 a 150 arbres estalvia un 20% de latència i la meitat de memòria a canvi d'un punt de recall (0.68 → 0.67) — un punt de recall són clients que abandonen sense que ningú els fes una oferta. Compensa? Amb p95 = 92 ms davant un pressupost de 300, no hi ha cap problema de latència que justifiqui pagar recall: CineClick es queda amb el campió intacte. La decisió contrària seria legítima amb un altre context (pressupost de 50 ms, o memòria cara): l'important és que ara és una decisió quantificada als dos plats de la balança, no un "vaig retallar arbres perquè anava lent". Si algun dia s'adopta una variant lleugera, entra pel camí de sempre: nova versió al registry, revisió humana, àlies.

Optimització 4: ONNX, un format pensat per a inferència

ONNX (Open Neural Network Exchange) és un format estàndard d'intercanvi de models: exportes el model entrenat a un graf d'operacions, i un runtime optimitzat (onnxruntime, en C++) l'executa. Per a serving aporta dues coses:

  • Inferència més ràpida, especialment en el cas que ens fa mal — poques files per crida — perquè elimina el sobrecost de Python/sklearn per predicció.
  • Serving sense scikit-learn: el contenidor d'inferència només necessita onnxruntime (desenes de MB davant l'stack sklearn+scipy+pandas), cosa que aprima la imatge i elimina la classe de bugs "versió de sklearn diferent en deserialitzar".
# scripts/exportar_onnx.py — exportar el campió a ONNX
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
import mlflow

model = mlflow.sklearn.load_model("models:/churn-cineclick@champion")

# ONNX exigeix declarar l'entrada: N files x n_features de tipus float
n_features = model.n_features_in_
tipus_entrada = [("entrada", FloatTensorType([None, n_features]))]

onx = convert_sklearn(model, initial_types=tipus_entrada)
with open("models/churn.onnx", "wb") as f:
    f.write(onx.SerializeToString())

# inferència amb el runtime (així el faria servir el servei)
import onnxruntime as rt
import numpy as np
sessio = rt.InferenceSession("models/churn.onnx")
X_exemple = np.random.rand(1, n_features).astype(np.float32)   # il·lustratiu
probs = sessio.run(None, {"entrada": X_exemple})[1]            # probabilitats

En proves amb el model de CineClick, onnxruntime redueix el predict d'1 fila de ~18 ms a ~2-4 ms. Els peatges, per valorar-ho honestament: és una peça més per mantenir (exportació al pipeline, verificar l'equivalència numèrica de les prediccions sklearn vs. ONNX abans de confiar-hi, algun operador no suportat segons el model), i les features continuen sent pandas — ONNX accelera el tram predict, no el pipeline sencer. CineClick ho deixa anotat com a següent palanca si el pressupost de latència s'estreny; avui, amb p95 a 92 ms, no paga la seva complexitat.

Optimització 5 (o no): CPU davant GPU

Cal dir-ho sense embuts: per a un RandomForest sobre dades tabulars, la GPU no compensa. Els arbres de decisió són ramificacions i comparacions — feina irregular en què la CPU és excel·lent — no les multiplicacions massives de matrius per a les quals la GPU existeix. Afegir GPU al servei de churn multiplicaria el cost de cada rèplica (una instància amb GPU costa diverses vegades una de CPU) per guanyar-hi poc o res, i complicaria imatge, drivers i scheduling a Kubernetes.

Quan SÍ que és conversa de GPU? Deep learning en inferència: transformers, visió, models d'embeddings — i fins i tot llavors només amb batching que ompli la GPU (les peticions d'1 en 1 la malbaraten; d'aquí els servidors d'inferència especialitzats amb micro-batching). L'heurística per al teu dia a dia:

Model Maquinari d'inferència raonable
Models tabulars (arbres, lineals, boosting) CPU, sempre. Optimitza amb les palanques 1–4
Xarxes petites / embeddings a baix volum La CPU sol bastar; mesura abans
Transformers/visió amb volum o latència exigent GPU amb batching (o endpoints gestionats)

I l'última palanca de cost ja la tens de 04-04: l'autoscaling ajusta les rèpliques al trànsit (pagar per demanda real), i per al job batch setmanal, còmput efímer o serverless amb escala a zero — un job que corre 10 minuts a la setmana no ha de tenir un servidor esperant-lo els altres 10.070 minuts.

La taula de palanques i el tancament del mòdul

El resum executiu de la lliçó:

Palanca Guany típic Cost d'implementació CineClick l'aplica?
1. Predicció vectoritzada / batch 10–400x en throughput massiu Gairebé nul (ja escrius bé el batch i /predir-lot) Sí, des del disseny
2. Memòria cau de respostes/features (TTL) Elimina el 100% del còmput a cada encert Baix (hash ja existent + dict/Redis); vigilar frescor Sí, TTL 300 s
3. Model més lleuger 20–50% latència, 50–95% memòria Mitjà: reentrenar i pagar mètrica (decisió de negoci) No — el recall val més que els ms
4. ONNX / runtime optimitzat 3–10x al tram predict Mitjà: exportació, verificació d'equivalència, peça extra Anotada per al futur
5. GPU Només deep learning amb batching Alt (cost d'instància, drivers, complexitat) No — tabular = CPU
Autoscaling / escala a zero (04-04) Pagar per demanda real Ja implementat (HPA); serverless per al batch

L'ordre no és casual: és rendibilitat decreixent. Les dues primeres palanques no costen gairebé res i les regala una bona arquitectura; les últimes exigeixen inversió i només es justifiquen amb mesures a la mà. I aquesta és la disciplina completa: mesurar (percentils) → localitzar (desglossament per trams) → aplicar la palanca més barata que resolgui → tornar a mesurar.

Fem balanç del mòdul 4. CineClick va triar els seus patrons amb criteri (batch per a màrqueting, online per a la cancel·lació), va construir el servei FastAPI amb contracte Pydantic, campió del registry i features de la font única, el va congelar a la imatge cineclick/churn-api:0.1.0, el va desplegar a Kubernetes amb 3–10 rèpliques, probes i dos nivells de rollback, i ara sap que respon amb p95 de 92 ms a 0,15 € les mil prediccions — mesurat, no suposat. Però repassa com ha arribat tot això a producció: la Laura va executar els tests a mà, tu vas construir la imatge a mà (docker build), la vas pujar a mà, vas aplicar els manifestos a mà (kubectl apply), i el job batch setmanal... encara el llança algú a mà un dilluns a les 6:00. Cada "a mà" és un oblit esperant que passi.

Errors Comuns i Consells

  • Error: optimitzar sense perfil. La intuïció diu "el model és el que va lent"; la mesura diu que la meitat del temps era pandas preparant una fila. Sense desglossament per trams, optimitzaràs el tram equivocat.
  • Error: reportar (i prometre) latències mitjanes. La mitjana amaga la cua; els usuaris viuen al p95 i al p99. Pressupostos i SLO sempre en percentils.
  • Error: puntuar en massa cridant l'API en bucle. 100.000 clients per /predir és una hora de peticions i càrrega inútil; la mateixa feina vectoritzada són segons. Cada patró (04-01) per la seva via.
  • Error: cacejar sense TTL (o amb un TTL alegre). Una memòria cau eterna converteix el teu servei online en un batch encobert i desactualitzat — just el que el cas de la cancel·lació no tolera. El TTL és una decisió de frescor, escriu-la i justifica-la.
  • Error: retallar el model sense quantificar la pèrdua. "Vaig baixar a 50 arbres i vola" — i el recall va caure de 0.68 a 0.60 sense que ningú posés aquest cost sobre la taula. La taula mida-latència-mètrica és obligatòria abans de decidir.
  • Error: demanar GPU per a un model tabular. És l'optimització més cara i menys útil de la llista. CPU + vectorització + memòria cau arriben lluníssim.
  • Consell: deixa la mesura executable al repo (scripts/mesurar_carrega.py): mesurar el rendiment ha de costar una comanda, perquè a 05-01 voldrem que sigui un pas més que s'executa sol.
  • Consell: acompanya cada proposta d'optimització amb el seu cost per 1.000 prediccions abans/després. És l'idioma comú entre enginyeria i negoci.

Exercicis

Exercici 1

Un servei reporta aquestes 10 latències (ms) sota càrrega: 22, 24, 25, 25, 26, 28, 30, 31, 45, 950. Calcula mitjana, p50 i p90 (amb el mètode de la lliçó: valor a la posició int(n × p) - 1 de la llista ordenada). Quina història explica cada mètrica i quina faries servir per verificar el pressupost de 300 ms?

Exercici 2

Màrqueting proposa reutilitzar /predir per puntuar els 80.000 clients de la campanya "perquè l'API ja està desplegada i escalada". Amb els números de la lliçó (p50 ≈ 38 ms per petició, throughput ≈ 210 req/s amb 20 de concurrència; predict_proba vectoritzat de 1.000 files ≈ 0,05 s), estima quant trigaria cada enfocament i escriu la recomanació.

Exercici 3

El pressupost de latència baixa de 300 ms a 60 ms (p95) perquè la pantalla de cancel·lació es redissenya. Amb el desglossament per trams i les taules de la lliçó, proposa un pla per ordre de rendibilitat per passar de p95 = 92 ms a p95 < 60 ms, indicant què NO faries encara.

Solucions

Solució 1

Ordenades ja ho estan. Mitjana = (22+24+25+25+26+28+30+31+45+950)/10 = 120,6 ms. p50 = posició int(10×0.5)-1 = 426 ms. p90 = posició int(10×0.9)-1 = 845 ms (i aquella petició de 950 ms viu al p100/màxim; amb només 10 mostres el p99 no és estimable amb serietat — una altra lliçó: els percentils alts necessiten moltes mostres). Històries: la mitjana (120 ms) suggereix un servei 4-5 vegades més lent del que experimenta l'usuari típic (p50 = 26 ms) — està segrestada per un únic outlier; el p90 diu que 9 de cada 10 peticions van en ≤45 ms. Per al pressupost de 300 ms es verifica el percentil compromès (p95/p99 amb mostres suficients): aquí l'única violació potencial és aquella cua de 950 ms, que és exactament el que caldria investigar i el que la mitjana dilueix i la p50 ignora.

Solució 2

  • Via API, seqüencial: 80.000 × 38 ms ≈ 3.040 s ≈ 51 minuts de peticions, ocupant el servei (i disparant l'HPA: pagaries rèpliques extra per autoinfligir-te càrrega).
  • Via API amb la concurrència mesurada: 80.000 / 210 req/s ≈ 380 s ≈ 6,3 minuts a throughput màxim — és a dir, saturant el servei que atén els usuaris reals de la pantalla de cancel·lació durant 6 minuts. Inacceptable en horari de servei.
  • Via batch vectoritzat (score_batch.py): 80 lots de 1.000 files × 0,05 s ≈ 4 segons de predict_proba (més lectura/escriptura de dades: total de l'ordre d'un minut), sense tocar l'API ni la seva infraestructura.

Recomanació: la campanya es puntua amb el job batch de 04-01 — que ja existeix, és tres ordres de magnitud més eficient i no competeix amb el trànsit online. "L'API ja està desplegada" no és un argument: l'API està dimensionada (3-10 rèpliques, pressupost de latència) per a un altre patró de consum.

Solució 3

Amb p95 = 92 ms i objectiu < 60 ms cal retallar ~35 ms. Per ordre de rendibilitat segons el desglossament (DataFrame ~8 ms + features ~9 ms + predict ~18 ms dominen):

  1. Memòria cau de respostes amb TTL (ja dissenyada): cada encert respon en <1 ms; amb un 20% d'encerts el p95 gairebé no baixa (els percentils alts són les fallades de memòria cau), així que ajuda al cost però no resol sola el p95 — cal atacar el camí sense memòria cau.
  2. Retallar el tram pandas (~17 ms): per a una fila, construir les features amb operacions sobre dict/numpy en lloc de DataFrame — però compte: això fregaria la reimplementació de features prohibida per 03-03; la via correcta és optimitzar construir_features al mateix features.py (que accepti també la ruta ràpida), mantenint la font única i amb tests d'equivalència. Guany estimat: 8-12 ms.
  3. ONNX per al predict (palanca 4): de ~18 ms a ~2-4 ms el tram del model, amb verificació d'equivalència numèrica abans de promocionar. Guany: ~14 ms. Amb els passos 2+3, el p95 estimat queda folgadament per sota de 60 ms.
  4. Què NO faria encara: sacrificar arbres/recall (palanca 3) — no es paga mètrica de negoci mentre quedin palanques purament tècniques — i, per descomptat, GPU (palanca 5), que no aplica a un model tabular. I després de cada canvi: tornar a passar mesurar_carrega.py, perquè les estimacions són hipòtesis fins que el percentil les confirma.

Conclusió

El mòdul 4 acaba on el curs va començar a prometre: el model de la Laura ja no viu en un notebook — atén peticions reals. El recorregut complet està dempeus i mesurat: patrons triats per consumidor (batch setmanal per a màrqueting, online per a la cancel·lació), servei FastAPI que valida amb Pydantic, carrega models:/churn-cineclick@champion i importa les features de la font única, imatge cineclick/churn-api:0.1.0 reproduïble i sense secrets, Kubernetes amb 3-10 rèpliques, probes contra /salut i dos rollbacks ben separats (imatge i model), i un rendiment verificat de p95 = 92 ms a 0,15 € per mil prediccions, amb les palanques d'optimització prioritzades i les descartades — GPU inclosa — descartades per escrit. Però fixa't en el pegament de tot això: mans. Tests que es passen si algú se'n recorda, docker build i push des del portàtil de torn, kubectl apply teclejat amb compte, un job batch que "algú llança els dilluns". Cada pas manual és lent, inauditable i — tard o d'hora — se saltarà just el dia que no tocava. El mòdul 5 automatitza el pegament: CI que executa tests de codi, de dades i de models a cada canvi, CD que construeix, publica i desplega la imatge sense mans, orquestració que llança el batch setmanal amb reintents i alertes, i estratègies de release (shadow, canary, A/B) perquè la versió nova es guanyi la confiança amb trànsit real abans de quedar-s'ho tot. Les peces ja existeixen; ara toca que es moguin soles.

© Copyright 2026. Tots els drets reservats