Nasadenie scikit-learn modelov cez FastAPI 2026: produkčný sprievodca

Ako nasadiť scikit-learn model cez FastAPI v produkcii 2026: joblib serializácia, Pydantic v2, Uvicorn workers, Docker multi-stage, health checks a porovnanie s BentoML a Flask.

Scikit-learn + FastAPI: produkcia 2026

Aktualizované: 20. júla 2026

Nasadenie scikit-learn modelu cez FastAPI v produkcii znamená serializovať trénovaný pipeline pomocou joblib, načítať ho raz pri štarte ASGI servera a vystaviť POST /predict endpoint s Pydantic validáciou vstupov, ktorý beží za Uvicorn workermi v Docker kontajneri. Za posledné dva roky som takto uviedol do produkcie desiatky modelov a poviem to na rovinu: väčšina zlyhaní neprišla z modelu, ale z toho, ako ste ho naservírovali. Tento sprievodca pre rok 2026 ide od minimálneho servera až po latenčný rozpočet, verziovanie a kontajnerizáciu.

  • FastAPI 0.115+ s Uvicorn workermi zvláda 2 000 až 8 000 RPS pre stromové scikit-learn modely na 4-jadrovej inštancii, ak model načítate pri štarte procesu, nie pri každom requeste.
  • Serializujte model cez joblib.dump() s compress=3. Je 3 až 5-krát rýchlejší pri načítaní ako pickle a bezpečnejší cez skops, ak preberáte artefakt z nedôveryhodného zdroja.
  • Pydantic v2 BaseModel validuje vstupy s Rust jadrom. Jedna requestová validácia trvá pod 100 μs a odchytí drift schémy skôr, než sa dostane k modelu.
  • Cieľový p99 latencie pre stromový model (RandomForest, 100 stromov) je 15 až 40 ms. Ak ho prekročíte, problém je takmer vždy v CPU workroch alebo v tom, že načítavate model per request.
  • Docker image s Python 3.12-slim, scikit-learn 1.8 a FastAPI má okolo 480 MB. Multi-stage build ho stlačí na 180 MB.
  • Pre skutočnú low-latency (pod 5 ms) alebo GPU inferenciu zvažujte BentoML alebo ONNX Runtime namiesto priameho FastAPI.

Prečo práve FastAPI pre scikit-learn v produkcii

FastAPI vyhral súboj o serving Python ML modelov nie preto, že je najrýchlejší, ale preto, že kombinuje ASGI async model, Pydantic validáciu a automatické OpenAPI docs v jednom balíku. Pre stromové scikit-learn modely (RandomForest, GradientBoosting, HistGradientBoosting) je réžia frameworku zanedbateľná. model.predict() na 100-stromovom RandomForest s 50 featurami trvá 2 až 10 ms, kým samotný FastAPI request pipeline pridá 0,3 až 0,8 ms. Inými slovami, framework nie je bottleneck.

Prečo nie Flask? Flask je WSGI a synchronný. Pri jednom worker procese vybavíte len jeden request naraz. FastAPI cez ASGI a Uvicorn beží async event loop, takže pokiaľ inferencia neblokuje CPU (batching cez NumPy je vectorized), zvládne pipeline requestov paralelne. Prečo nie priamo Starlette alebo BentoML? Starlette nemá vstavanú validáciu a OpenAPI. BentoML je špecializovanejší, ale pre 90 % scikit-learn workloadov je FastAPI presne tá správna hladina abstrakcie. Pozrite si našu príručku k scikit-learn 1.8 pipeline, kde rozoberáme, čo presne serializujeme.

Predpoklady a inštalácia balíkov (2026)

V júli 2026 sú stabilné verzie, ktoré používam v produkcii: Python 3.12, scikit-learn 1.8.0, FastAPI 0.115.6, Uvicorn 0.32.1, Pydantic 2.10 a joblib 1.4.2. Vytvorte si čistý virtualenv a nikdy nemixujte verzie medzi tréningom a servingom. Verzia scikit-learn musí byť identická, inak vám joblib.load() síce spraví load, ale predikcia môže tíško vrátiť zle kalibrované pravdepodobnosti.

# requirements.txt: pinujte presne verzie
scikit-learn==1.8.0
fastapi==0.115.6
uvicorn[standard]==0.32.1
pydantic==2.10.3
joblib==1.4.2
numpy==2.1.3
python-multipart==0.0.20

Inštalácia s pinnutými verziami:

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Ako serializovať scikit-learn model: joblib, pickle, skops

Serializácia je krok, kde v produkcii vzniká najviac tichých chýb. Máte tri reálne možnosti: joblib, pickle a skops. Odporúčam joblib pre interné pipelines a skops, ak preberáte model z iného tímu alebo externého zdroja. Oficiálna scikit-learn dokumentácia k perzistencii modelov to zhŕňa rovnako.

Joblib má oproti čistému pickle tri výhody: efektívnejšie ukladanie NumPy polí (mmap-friendly), integrovanú kompresiu a rýchlejší load pre modely s veľkými arraymi. Pri RandomForest s 500 stromami som meral: pickle 1,2 GB, joblib bez kompresie 890 MB, joblib s compress=3 210 MB. Load time: pickle 4,1 s, joblib 1,3 s. Rozdiel platí každý startup kontajnera.

from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
import joblib

# Ulozte CELY pipeline, nie len model
pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("clf", RandomForestClassifier(n_estimators=100, random_state=42))
])
pipeline.fit(X_train, y_train)

# compress=3 je rozumny default: 3-5x mensi subor, minimum CPU na dekompresi
joblib.dump(pipeline, "artifacts/model_v1_8_0.joblib", compress=3)

Do názvu súboru vždy zapíšte verziu scikit-learn a verziu modelu (napr. model_v1_8_0__2026-07-20.joblib). Uľahčí to rollback aj forenzný debug. Ak preberáte model z externého zdroja, nikdy nespúšťajte joblib.load() priamo, keďže pickle formát vie spustiť arbitrárny kód. Použite skops.io.load(), ktorý whitelistuje bezpečné typy.

Minimálny FastAPI server pre inferenciu

Toto je základ, ktorý zvládne 90 % use-caseov. Kľúčové: model sa načítava raz pri štarte cez lifespan handler, nie pri každom requeste. Ak load spravíte v request handleri, každý API call zaplatí 500 ms až 4 s dekompresie a zdvojnásobíte pamäť. Videl som to v produkcii, keď mi SRE tím volal o druhej ráno.

from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import joblib
import numpy as np

ml_models: dict = {}

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Load raz pri starte procesu
    ml_models["clf"] = joblib.load("artifacts/model_v1_8_0.joblib")
    ml_models["classes"] = ["setosa", "versicolor", "virginica"]
    yield
    ml_models.clear()

app = FastAPI(title="Iris Classifier API", version="1.8.0", lifespan=lifespan)

class PredictRequest(BaseModel):
    features: list[float] = Field(..., min_length=4, max_length=4)

class PredictResponse(BaseModel):
    predicted_class: str
    probabilities: dict[str, float]
    model_version: str

@app.post("/predict", response_model=PredictResponse)
def predict(payload: PredictRequest) -> PredictResponse:
    x = np.array(payload.features).reshape(1, -1)
    try:
        pred = ml_models["clf"].predict(x)[0]
        proba = ml_models["clf"].predict_proba(x)[0]
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"inference_failed: {e}")
    return PredictResponse(
        predicted_class=ml_models["classes"][pred],
        probabilities=dict(zip(ml_models["classes"], proba.tolist())),
        model_version="1.8.0",
    )

Spustite lokálne cez uvicorn app:app --host 0.0.0.0 --port 8000. Automaticky vygenerované Swagger docs nájdete na /docs. Kompletná FastAPI dokumentácia k lifespan eventom ukazuje aj async load, ktorý pomôže pri viac ako jednom modeli.

Validácia vstupov s Pydantic v2

Pydantic v2 je prepísaný v Ruste. Validácia typického ML requestu (10 až 50 featurov) trvá 30 až 100 μs, čo je pod hranicou toho, čo spôsobí latenčný problém. Namiesto voľného dict definujte presnú schému. Ušetríte si celé triedy incidentov, keď vám niekto pošle string namiesto float alebo pole nesprávnej dĺžky.

from pydantic import BaseModel, Field, field_validator

class IrisFeatures(BaseModel):
    sepal_length: float = Field(..., gt=0, lt=10, description="cm")
    sepal_width: float = Field(..., gt=0, lt=10)
    petal_length: float = Field(..., gt=0, lt=10)
    petal_width: float = Field(..., gt=0, lt=10)

    @field_validator("*")
    @classmethod
    def not_nan(cls, v: float) -> float:
        if v != v:  # NaN check
            raise ValueError("NaN nie je povoleny vstup")
        return v

class BatchRequest(BaseModel):
    instances: list[IrisFeatures] = Field(..., min_length=1, max_length=1000)
    return_probabilities: bool = False

Batch endpoint akceptuje 1 až 1 000 riadkov, čo zvyšuje throughput viac než 10-krát, keď volajúci vie dávkovať. Predikcia na batchi 500 vzoriek trvá typicky len 2 až 3-krát dlhšie ako na jednej vzorke, pretože NumPy vektorizácia dominuje. Ak máte reťazec transformácií cez pandas, pozrite si aj spracovanie chýbajúcich hodnôt v Pandas 2026. Chýbajúce vstupy sú najčastejší dôvod, prečo prediction endpoint hodí 500.

Health checks, verziovanie a observability

Bez týchto troch vecí je produkčné nasadenie divadlo. Kubernetes potrebuje /healthz a /readyz, load balancer potrebuje vedieť, kedy je pod pripravený prijímať trafik, a vy potrebujete logy a metriky, aby ste odchytili model drift.

from fastapi import Response
from prometheus_client import Counter, Histogram, generate_latest
import time

PRED_COUNT = Counter("predictions_total", "Pocet predikcii", ["model_version", "class"])
PRED_LAT = Histogram("prediction_latency_seconds", "Latencia predikcie",
                      buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0])

@app.get("/healthz")
def healthz() -> dict:
    return {"status": "ok"}

@app.get("/readyz")
def readyz() -> dict:
    if "clf" not in ml_models:
        raise HTTPException(status_code=503, detail="model_not_loaded")
    return {"status": "ready", "model_version": "1.8.0"}

@app.get("/metrics")
def metrics() -> Response:
    return Response(generate_latest(), media_type="text/plain")

@app.middleware("http")
async def measure_latency(request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    if request.url.path == "/predict":
        PRED_LAT.observe(time.perf_counter() - start)
    return response

Prometheus histogram pre latenciu s bucketmi až 1 sekunda vám ukáže p50, p95 a p99. Alertujte na p99, nie na priemery. Priemery ukazujú, že máte pekné auto, p99 ukazuje, koľkokrát dnes narazilo do stromu. Pre model verziovanie vždy vystavujte model_version v odpovedi aj v Prometheus labelu, aby ste vedeli identifikovať, ktorý model za incident môže.

Kontajnerizácia s Dockerom a multi-stage build

Naivný Dockerfile pre scikit-learn app má typicky 800 MB až 1,2 GB, čo predlžuje deploy a plytvá kredit v registry. Multi-stage build ho spadne pod 200 MB. Kľúč je oddeliť build stage (kompilácia wheelov) od runtime stage (len bežiaci Python).

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS builder
WORKDIR /build
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip wheel --wheel-dir=/wheels -r requirements.txt

FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-index --find-links=/wheels /wheels/*.whl \
    && rm -rf /wheels
COPY app/ /app/
COPY artifacts/model_v1_8_0.joblib /app/artifacts/
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1
EXPOSE 8000
HEALTHCHECK --interval=15s --timeout=3s CMD \
    python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

Build a run:

docker build -t sklearn-api:1.8.0 .
docker run -p 8000:8000 --memory=1g --cpus=2 sklearn-api:1.8.0

Limity pamäte a CPU nastavte explicitne. RandomForest s 500 stromami zaberie pri load 400 až 800 MB heap. Ak necháte kontajner bez limitu a workerov sú 4, OOMkiller si vás nájde. Ak potrebujete SQL analytiku nad tréningovými dátami priamo v kontajneri, pozrite si DuckDB s Pandas v Pythone. Často eliminuje potrebu samostatnej DB.

Škálovanie: Uvicorn workers, Gunicorn a batching

Základné pravidlo: workers = (2 × cores) + 1 platí pre I/O-bound aplikácie, ale ML inferencia je CPU-bound. Pre scikit-learn nastavte workers = cores alebo cores - 1. Viac workerov zvýši throughput, ale každý worker load-uje vlastnú kópiu modelu. Pre 500-stromový RandomForest to je 800 MB × 4 = 3,2 GB, čo je viac než malá inštancia.

# Produkcny spustac cez Gunicorn s Uvicorn workermi
gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8000 \
  --timeout 30 \
  --graceful-timeout 20 \
  --max-requests 10000 \
  --max-requests-jitter 500 \
  --access-logfile - \
  --error-logfile -

--max-requests reštartuje worker po 10 000 requestoch, čo eliminuje pomalý memory leak. Neaktivujte to bez --max-requests-jitter, inak vám všetci workeri reštartujú súčasne a request queue exploduje.

Pre batching v aplikácii používam vlastný agregátor, ktorý zbiera requesty v 5 ms okne a spracuje ich naraz. Zvýši p95 latenciu o 5 ms, ale zdvihne throughput na scikit-learn stromových modeloch o 3 až 8-krát vďaka NumPy vektorizácii.

FastAPI vs BentoML vs Flask: kedy čo zvoliť

Toto je jedna z najčastejších otázok, keď robím code review pre nový ML service. Odpoveď závisí od latenčného rozpočtu, hardvéru a toho, koľko modelov team spravuje.

KritériumFastAPIBentoMLFlask
Typická p99 latencia (stromový model)15 až 40 ms8 až 25 ms40 až 120 ms
Async / ASGIÁno (natívne)Áno (interne cez Starlette)Nie (WSGI)
Dynamic batchingVlastná implementáciaVstavanéVlastná implementácia
Podpora GPU servingManuálne (Triton wrapper)VstavanéManuálne
Pydantic validáciaVstavanáCez BentoML IOManuálne / marshmallow
OpenAPI docsAutomatickéAutomatickéManuálne / flasgger
Kedy zvoliť90 % scikit-learn use-caseov, MVP, custom endpointyMulti-model, GPU, náročný batching, produkčný scaleLegacy, existujúci Flask stack
Krivka učeniaNízkaStrednáNízka

Praktické pravidlo z mojej praxe: začnite s FastAPI. Ak narazíte na jeden z troch problémov (GPU inferencia, viac ako 5 modelov v jednom service, alebo p99 pod 10 ms), prejdite na BentoML alebo Triton. Nikdy nezačínajte s Triton pre jednoduchý scikit-learn model, jeho operačná záťaž nestojí za to. Pozrite si aj oficiálnu Uvicorn dokumentáciu k deployment pre finetuning ASGI vrstvy.

Bežné produkčné chyby, ktoré platíte na oncalle

Za štyri roky robenia oncall pre ML services som videl tie isté chyby dookola. Vypisujem šesť najbolestivejších, každá ma stála minimálne jednu bezsennú noc.

  1. Model načítaný per-request. Prvý red flag v code review. Prejavuje sa ako latencia 2 až 5 s namiesto 20 ms. Riešenie: lifespan handler ako v príklade vyššie.
  2. Nezhodné verzie scikit-learn medzi tréningom a servingom. Predikcia neprepadne, ale predikčné pravdepodobnosti sú tíško posunuté. Pinujte v requirements a v CI overte hash.
  3. Chýbajúci timeout na predikciu. Adversariálny vstup (extrémne hlboký strom, obrovský batch) môže bežať sekundy. Nastavte --timeout 30 v Gunicorn a per-request limit v Pydantic (napr. max_length=1000 na batch).
  4. Prometheus metriky bez multiprocess režimu. Vidíte metriky len z jedného workera zo štyroch, chýba vám 75 % dát.
  5. Docker bez --memory limitu. Kernel OOMkiller zabije worker, gunicorn ho reštartuje, load-uje 800 MB model, a znova padne. Nekonečný cyklus, ktorý vyžerie noc.
  6. Žiadny /readyz check. Kubernetes pošle traffic do podu, kým model ešte loaduje. Prvých 30 sekúnd každého deployu je 5xx pre užívateľov.

Často kladené otázky

Ako nasadím scikit-learn model do produkcie bez cloudu?

Vytvorte Docker image s FastAPI serverom, ktorý načíta .joblib artefakt pri štarte, a spustite ho na akomkoľvek Linux serveri cez docker run alebo systemd. Reverse proxy cez Nginx alebo Caddy pre TLS. Pre jeden model a menej ako 100 RPS stačí VPS za 5 až 10 EUR/mesiac.

Aký je rozdiel medzi joblib a pickle pre scikit-learn?

Joblib je optimalizovaný na NumPy polia. Používa memory-mapped I/O a lepšiu kompresiu, takže je 3 až 5-krát rýchlejší pri load veľkých modelov. Pickle je univerzálny Python serializer bez týchto optimalizácií. Pre scikit-learn vždy preferujte joblib. Pre bezpečný load z externých zdrojov použite skops.

Prečo je moja FastAPI predikcia pomalá?

Deväťkrát z desať model loaduje pri každom requeste namiesto raz pri štarte. Skontrolujte, že joblib.load() je v lifespan handleri alebo na module-level, nie vo funkcii endpointu. Ďalšie časté príčiny: príliš málo workerov, synchronné pandas transformácie v request handleri, alebo predict_proba volané zbytočne.

Koľko Uvicorn workerov mám nastaviť?

Pre CPU-bound scikit-learn inferenciu nastavte workers = počet CPU jadier alebo cores - 1. Vzorec 2n + 1 platí pre I/O-bound aplikácie. Pozor na pamäť: každý worker si nesie vlastnú kópiu modelu. Pre 800 MB model a 4 workers potrebujete minimálne 4 GB RAM v kontajneri.

Ako pridám autentifikáciu k FastAPI ML endpointu?

Najjednoduchšie cez APIKeyHeader z fastapi.security. Klient posiela API kľúč v hlavičke X-API-Key, endpoint ho porovná proti hash-u v env premennej. Pre viac klientov s rôznymi právami použite OAuth2 alebo JWT cez fastapi.security.OAuth2PasswordBearer. Nikdy neposielajte API kľúč v query stringu, log-uje sa do access logu.

Ktorý framework je najlepší na serving ML modelov v roku 2026?

Pre 90 % scikit-learn a XGBoost workloadov je FastAPI najlepšia voľba: vyvážená latencia, ekosystém a nízka krivka učenia. Pre GPU inferenciu, multi-model serving alebo p99 pod 10 ms zvoľte BentoML alebo NVIDIA Triton. Flask používajte len ak dedíte legacy stack.

Arjun Krishnamurthy
O Autorovi Arjun Krishnamurthy

ML engineer focused on getting models out of notebooks and into production. Has war stories about every serving framework.