A FastAPI ML modell deployment a legrövidebb út egy betanított scikit-learn, XGBoost vagy PyTorch modelltől a production REST API-ig Pythonban. A FastAPI ASGI alapú async webkeretrendszer, ami natívan kezeli a Pydantic validációt, automatikus OpenAPI dokumentációt generál, és uvicorn workerekkel skálázódik. Ebben az útmutatóban végigvezetek egy production-ready setupot (lifespan modell betöltés, async batching, Prometheus metrikák, Docker konténer), abból kiindulva, amit az elmúlt négy évben tanultam az on-call éjszakákról.
Őszintén szólva, az első saját ML API-mat 2022-ben raktam élesbe, és három órán át bámultam, hogy miért 502-zik a load balancer. Kiderült, a warmup hiányzott. Az ilyen aprócska részleteken áll vagy bukik a production deploy, szóval lássunk neki.
A FastAPI lifespan context manager az egyetlen helyes hely a modell betöltésére. Ne tedd a request handlerbe, mert minden hívásnál újratölti és felrobbantja a p99 latenciát.
CPU-bound inferencia (scikit-learn, XGBoost) def endpointot kapjon, ne async def-et, különben blokkolja az event loopot és sorbaáll minden kérés.
Production setupban Pydantic v2 schemák, uvicorn --workers N (ahol N = CPU cores), és reverse proxy (nginx vagy Caddy) elé tett gunicorn UvicornWorkerrel a stabil minta.
Request batching 5–20 ms-os ablakkal 3–8× throughputot ad GPU inferenciánál, CPU modelleknél 1,5–2×. Érdemes mérni a saját workloadon.
Prometheus /metrics endpoint, /healthz liveness és /readyz readiness probe nélkül a Kubernetes deploy garantáltan flapelni fog.
A leggyakoribb production hiba nem a modell, hanem a hideg start. Konténer újraindításkor 8–15 másodperc, amíg a joblib pickle betölt, és előmelegítés nélkül a load balancer kérdőjelet rajzol az új podra.
Miért FastAPI ML modell servinghez?
A FastAPI 2026-ra de facto standard lett Python ML inferencia API-khoz, mert három dolgot csinál jól, amit a Flask már nem győz: natív ASGI async támogatást ad (uvicorn alatt), Pydantic v2 validációval típusbiztos request/response sémákat generál, és OpenAPI 3.1 dokumentációt buildel automatikusan az endpointokból. Ehhez jön a startup overhead. Egy üres FastAPI app 40–60 ms alatt indul, ami konténer cold start mellett kritikus.
Production ML kontextusban a választás nem "FastAPI vs Flask", hanem "FastAPI vs dedikált modell server". Az alábbi táblázat azokat a tradeoff-okat foglalja össze, amik a legtöbb projektnél felmerülnek.
Szempont
FastAPI
Flask
BentoML
Ray Serve / TorchServe
Async támogatás
Natív (ASGI)
Nincs (WSGI)
Igen
Igen
Auto OpenAPI doksi
Igen (3.1)
Külső lib kell
Igen
Részleges
Tanulási görbe
Alacsony
Nagyon alacsony
Közepes
Meredek
Beépített request batching
Nincs (kézi)
Nincs
Van
Van
Multi-model serving
Manuális
Manuális
Van
Van
GPU inferencia overhead
Minimális
Minimális
Alacsony
Optimalizált
P99 latencia (10 KB req)
8–15 ms
20–35 ms
10–18 ms
6–12 ms
Mikor a legjobb
Klasszikus ML, közepes QPS
Egyszerű prototípus
Multi-model, MLOps
GPU, magas QPS
A való életben a legtöbb csapatnak FastAPI a megfelelő középút, főleg ha scikit-learn pipeline-okat vagy gradient boosting modelleket szolgál ki. GPU-n futó nagy modellekhez (LLM, vision) érdemes Ray Serve vagy vLLM felé nézni. A scikit-learn oldalra korábban írt scikit-learn Pipeline és ColumnTransformer útmutató jó kiindulópont a betanított pipeline pickle-elésére, mielőtt deployolnád.
Production-ready FastAPI projekt struktúra
A demók és tutorialok jellemzően egyetlen main.py fájlban tartják a modellt, sémát és endpointot. Production-ben ez két hónap múlva karbantarthatatlan, garantálom. A bevált struktúra szétválasztja a modell wrappert, sémákat, dependency injectiont és az API route-okat. Az alábbi layout négy különböző csapatomnál is bevált.
A pyproject.toml-ban érdemes lockfile-t használni (uv vagy poetry), mert ha a production konténer építésekor a pip másik scikit-learn verziót húz, mint a tanításnál, a pickle deszerializálás hibára, vagy ami rosszabb, csendes prediction shiftre megy. A korábbi XGBoost vs LightGBM vs CatBoost összehasonlítás kitér ezekre a verzió-érzékenységekre.
Modell betöltés lifespan context managerrel
A FastAPI 0.93 óta a lifespan context manager a hivatalos minta startup/shutdown logikára, és az @app.on_event dekorátor deprecated. Ez az egyetlen helyes hely a modell betöltésére: a process indulásakor egyszer fut le, a memóriában tartott objektum pedig elérhető a dependency injectionön keresztül minden requestben. Egy 250 MB-os XGBoost booster betöltése 2–4 másodperc, és ezt nem akarod minden POST kéréskor megismételni.
from contextlib import asynccontextmanager
from fastapi import FastAPI
import joblib
from app.ml.model import ModelWrapper
ml_state: dict = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: betöltjük a modellt és feature pipeline-t
print("Modell betöltése...")
pipeline = joblib.load("artifacts/pipeline_v3.2.1.joblib")
ml_state["model"] = ModelWrapper(pipeline, version="3.2.1")
# Warmup: első predikció soha nem 5 ms, csináljunk egy dummy inferenciát
ml_state["model"].warmup()
print(f"Modell kész, verzió: {ml_state['model'].version}")
yield
# Shutdown: graceful cleanup
ml_state.clear()
print("Modell kiürítve.")
app = FastAPI(lifespan=lifespan, title="Churn Prediction API")
A warmup lépés a tapasztalat alapján kritikus. Az első scikit-learn vagy XGBoost inferencia 30–150 ms, a következő 1–3 ms, mert addigra a lazy importok lefutnak, a NumPy buffereket lefoglalja, és a CPU cache feltöltődik. Production load balancer mögött az első három request mindig olyan podra esik, ami warmup nélkül 502-vel timeoutol. Szóval készíts egy dummy feature vektort, és hívd meg a predict()-et a lifespanben. Pont ezt szúrtam el az első deployomnál.
Pydantic schemák a request/response validációhoz
A Pydantic v2 (a FastAPI ≥0.100 alapértelmezett) Rust alapú validációs magot (pydantic-core) használ, ami 10–50× gyorsabb a v1-nél. ML inferencia kontextusban két dolgot vársz tőle: típusbiztos input séma (nem kell request.json()-t parse-olni kézzel) és tiszta hibaüzenet rossz inputra. Ne hagyatkozz a default mezőkre a feature-eknél. Ha az ügyfél elfelejti az age-et, jobb 422-t kapni, mint csendesen 0-val prediktálni.
Az extra="forbid" beállítás a tapasztalat alapján kifizetődik: ha az ügyfél elgépeli a mezőnevet (tenure_month a helyes tenure_months helyett), 422-t kap a default 0 helyett, és nem rakja tönkre a downstream metrikákat. A Literal típus kategorikus változókra szigorúbb validációt ad, mint az enum, és a generált OpenAPI doksiban legördülő menüként jelenik meg.
Async vs sync endpointok: melyiket válaszd?
Ez az a kérdés, ami a legtöbb félreértést okozza, és aminek rossz megválaszolása p99 latencia katasztrófákhoz vezet. A szabály egyszerű: ha az endpoint CPU-bound műveletet hajt végre (modell predikció NumPy-val), írj sima def-et. A FastAPI külön threadpool workeren futtatja, és nem blokkolja az event loopot. Ha IO-bound (Postgres lekérdezés, S3 letöltés, HTTP hívás másik service-hez), async def-et használj await-tel.
A "ROSSZ" verzió alacsony terhelésen működik, de amint a párhuzamos requestek száma a uvicorn worker számát meghaladja, az event loop megáll a NumPy számításon, és minden bejövő request sorbaáll. Production loadon ez azt jelenti, hogy a p50 lehet 8 ms, de a p99 átszalad 2000 ms fölé. A FastAPI hivatalos async dokumentációja részletesen kifejti, hogy a framework miért futtatja a sync def-eket threadpoolban.
Batch inferencia és request batching
Két különböző batch fogalom létezik, és sokan összekeverik őket. A kliens oldali batch az, amikor a hívó fél egy listát küld be, és a server egyetlen predict() hívással válaszol. A szerver oldali (dynamic) batching az, amikor a server 5–20 ms-os ablakban összegyűjti a párhuzamosan érkező egyedi requesteket, és batch-ben futtatja a modellen, hogy kihasználja a vektorizációt vagy a GPU-t.
from typing import List
class BatchChurnRequest(BaseModel):
requests: List[ChurnRequest] = Field(..., min_length=1, max_length=512)
class BatchChurnResponse(BaseModel):
predictions: List[ChurnResponse]
batch_size: int
total_time_ms: float
@router.post("/predict/batch", response_model=BatchChurnResponse)
def predict_batch(payload: BatchChurnRequest, model: ModelWrapper = Depends(get_model)):
start = time.perf_counter()
# NumPy-szerű batch input, egyetlen .predict() hívás
df = pd.DataFrame([r.model_dump() for r in payload.requests])
probas = model.predict_proba_batch(df)
elapsed = (time.perf_counter() - start) * 1000
return BatchChurnResponse(
predictions=[
ChurnResponse(
customer_id=r.customer_id,
churn_probability=float(p),
risk_label="high" if p > 0.7 else "medium" if p > 0.3 else "low",
model_version=model.version,
inference_time_ms=elapsed / len(probas),
predicted_at=datetime.utcnow(),
)
for r, p in zip(payload.requests, probas)
],
batch_size=len(probas),
total_time_ms=round(elapsed, 2),
)
Mértem egy LightGBM modellen: egyedi predikció 1,8 ms, 100-as batch 6,4 ms, vagyis 64 µs per record. Ha az ügyfél tud batch-elni, mindenképp tegye. Server oldali dynamic batchinghez egy aszinkron queue és egy collector task kell. A FastAPI-ban kézzel kell megírni, de létezik a fastapi-batch külső lib is. GPU-s modelleknél (transformers, CNN) viszont kötelező, mert ott a batch dimenzió szinte ingyen van.
Monitoring, Prometheus metrikák és health checkek
Egy ML API health endpoint nélkül nem deploy-olható Kubernetesre vagy ECS-re, mert a load balancer nem fogja tudni, hogy az új pod kész-e forgalmat fogadni. Két külön probe kell: a liveness (/healthz) azt mondja meg, hogy az alkalmazás fut és nem kell újraindítani, a readiness (/readyz) pedig azt, hogy a modell betöltődött és fogad forgalmat. Ha ezt összekevered, a Kubernetes újraindítja a podot, miközben csak warmup folyamatban van.
from fastapi import status
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
from starlette.responses import Response
PRED_COUNT = Counter("predictions_total", "Predikciók száma", ["model_version", "risk_label"])
PRED_LATENCY = Histogram(
"prediction_latency_seconds",
"Predikciós latencia eloszlás",
buckets=(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0),
)
@router.get("/healthz", status_code=status.HTTP_200_OK)
def liveness():
return {"status": "alive"}
@router.get("/readyz")
def readiness(model: ModelWrapper = Depends(get_model_or_none)):
if model is None or not model.is_ready():
raise HTTPException(status_code=503, detail="Model not ready")
return {"status": "ready", "model_version": model.version}
@router.get("/metrics")
def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
A Prometheus Histogram bucketjeit szándékosan 1 ms-tól 1 s-ig állítottam. Egy klasszikus ML modellnél nincs értelme másodperces bucketokat tartani, csak elnyomják a felbontást. A predictions_total címkékkel (model_version, risk_label) később könnyű Grafana boardot építeni rá: idősoros átlag latencia, predikciós eloszlás drift, percentilis lebontás. A prometheus_client hivatalos repó részletesen dokumentálja a metric típusokat. Egy middleware-rel a PRED_LATENCY.observe()-ot automatikusan be lehet kötni minden endpointra.
Docker konténer és deployment lépésről lépésre
Egy production FastAPI image építésénél három dolog számít: kis méret (gyors pull), reproducibility (lockfile), és nem-root user. A multi-stage build minimalizálja a végső image-et. A builder stage húzza a wheel-eket, a runtime stage csak a futtatáshoz szükséges fájlokat tartalmazza. 2026-ban a Python 3.12-slim alap image a sweet spot: 50 MB alapból, és a 3.13-mal ellentétben minden lib teszteltebb.
A worker szám döntés érzékenyebb, mint elsőre tűnik. Ha a modell 250 MB-ot foglal és 2 GB RAM-ot adsz a konténernek, akkor 4 worker = 1 GB modell-másolat, mert a forking miatt minden worker külön memóriát kap, kivéve ha --preload-ot használsz gunicornnal. Sweet spot egy 4 vCPU node-on általában 2–3 uvicorn worker, plusz a FastAPI threadpool default 40 szállal. A uvicorn deployment dokumentáció részletesen tárgyalja a gunicorn + UvicornWorker mintát is, ami nagy QPS-re érettebb.
Modell verziókezelés és A/B tesztelés
Production rendszerben sosem csak egy modell van. Van a jelenlegi (champion), van egy új jelölt (challenger), és van legalább egy rollback verzió. A naiv minta "cseréljük a pickle fájlt", ami két dolog miatt fáj: nincs azonnali rollback (újra kell deployolni), és nincs A/B forgalom-szétosztás. A megoldás egy verziókezelt registry plus header-alapú routing.
from fastapi import Header
from typing import Optional
import hashlib
# Registry: verzió → ModelWrapper
MODELS: dict[str, ModelWrapper] = {}
def select_model(
x_model_version: Optional[str] = Header(default=None),
customer_id: Optional[str] = None,
) -> ModelWrapper:
# 1. Explicit header felülír mindent (debug + canary)
if x_model_version and x_model_version in MODELS:
return MODELS[x_model_version]
# 2. Customer ID alapú stabil A/B (5% challenger forgalom)
if customer_id:
bucket = int(hashlib.md5(customer_id.encode()).hexdigest(), 16) % 100
if bucket < 5 and "challenger" in MODELS:
return MODELS["challenger"]
# 3. Default: champion
return MODELS["champion"]
A hash-alapú bucketing kulcs eleme, hogy stabil: ugyanaz a customer mindig ugyanabba a bucketbe esik, így a metrikák nem zajosodnak el. A FastAPI minden response-ba érdemes belerakni a X-Model-Version headert, hogy a kliens loggolhassa, melyik modell válaszolt. Ez teszi auditálhatóvá az A/B kísérletet. Modell registryt érdemes MLflow vagy DVC alá tenni, de S3 + JSON manifest minimum is működik.
Latencia optimalizálás és gyakori hibák
A négy leggyakoribb production performance hiba, amit ML serving FastAPI projekteknél láttam, és a konkrét megoldásuk:
JSON serialization a bottleneck. Nagy response (1000+ predikció) esetén a default json modul 30–60 ms-ot vehet el. Csere orjson-ra: FastAPI(default_response_class=ORJSONResponse), ami 3–6× gyorsabb, és a NumPy float-okat natívan kezeli.
Pandas DataFrame minden kéréshez. Egy egyedi predikció kedvéért nem kell DataFrame-et építeni, közvetlenül NumPy array-jel hívd a model.predict()-et. 0,5–1,5 ms-ot megspórolsz per request.
Logging IO blokkol. A default Python logging szinkron, és ha minden requestnél fájlba írsz, az IO blokkolja a worker-t. Használj queue_handler-t vagy strukturált stdout JSON-t, amit a konténer runtime gyűjt.
Túl sok worker, kevés thread. 16 uvicorn worker 32 GB RAM mellett kicsi modellnél jó ötlet. Nagy modellnél (1+ GB) viszont szétfeszíti a memóriát. Inkább 2–4 worker, és emeld a threadpoolt asyncio.to_thread alapokra.
A profilozáshoz a py-spy top módja indokolatlanul hasznos. Flame graph nélkül is megmutatja, melyik függvényben tölt időt a process. Egyszer egy kollégámmal négy órán át hibakerestünk egy lassú endpointot, mire kiderült, hogy a datetime.utcnow() deprecated warning-ot dobott minden response-on, és a warning filter kapálózott. A py-spy 30 másodperc alatt megtalálta volna.
Gyakran ismételt kérdések
Mennyi memória kell egy FastAPI ML serving konténernek?
A modell mérete plusz uvicorn worker számszor 200–400 MB Python overhead. Egy 250 MB-os scikit-learn pipeline 2 workerrel kényelmesen elfut 1 GB RAM-on, 4 workerrel inkább 2 GB-ot kérj. GPU modelleknél (PyTorch) a CUDA context önmagában 800 MB–1,2 GB.
FastAPI vagy Flask: melyik a jobb 2026-ban ML deploymenthez?
FastAPI minden új projektnél. A Flask továbbra is működik egyszerű prototípusokra, de hiányzik az async támogatás, a Pydantic validáció és az auto-OpenAPI dokumentáció. Ezeket 2026-ban kézzel pótolni felesleges karbantartási teher.
Hogyan tudom csökkenteni a cold start időt FastAPI ML konténerben?
Három fő technika: (1) használj joblib helyett ONNX vagy Treelite formátumot, ami 3–5× gyorsabban betöltődik; (2) tartsd a konténer image méretét 500 MB alatt slim base image-zsel és multi-stage build-del; (3) Kubernetes-ben állíts be startupProbe-t hosszabb timeouttal, hogy a readiness ne flapeljen warmup közben.
Lehet GPU inferenciát FastAPI-val csinálni?
Igen, de érdemes mérlegelni. Egyetlen modellnél FastAPI + PyTorch CUDA jól működik, request batchinggel együtt. Nagy LLM-eknél vagy magas QPS-nél a Ray Serve, TorchServe vagy vLLM dedikált GPU scheduling-je hatékonyabb, és kevesebb fejfájást okoz a memória menedzsment és a token streaming.
Hogyan teszteljem a FastAPI ML endpointot CI-ben?
A FastAPI TestClient (httpx alapú) szinkron unit tesztekhez, az AsyncClient async tesztekhez. Töltsd be a modellt egyszer a pytest fixture-ben (scope="session"), és teszteld a happy path mellett a 422-es validáció hibákat, a 503-as readyz-t modell hiányában, és legalább egy szerződéses (contract) tesztet a response sémára.
Kell uvicorn vagy gunicorn production-ben?
Kis-közepes terhelésen uvicorn --workers N elég. Magasabb QPS-en gunicorn UvicornWorkerrel stabilabb, mert jobban kezeli a worker újraindítást, hot reloadot és a graceful shutdownt. Mindkettő mögé reverse proxy (nginx, Caddy) kell TLS terminációra és statikus tartalom kiszolgálására.