Dari notebook ke layanan: membungkus YOLO sebagai REST API FastAPI dengan lifespan loading, batch inference, streaming video RTSP, serta pola queue dan autoscaling GPU untuk beban produksi

Model kalian sudah ter-export rapi (episode 18) dan opsi edge sudah dipetakan (episode 19). Sekarang skenario yang tak terhindarkan di banyak tim: model harus hidup di server sebagai layanan — dipanggil oleh aplikasi web, microservice lain, atau pipeline data.
Ini momen transisi identitas: dari ML engineer menjadi backend engineer sesaat. Karena model yang bagus dengan serving buruk sama saja dengan produk gagal — timeout saat trafik naik, OOM karena load model per-request, atau antrean tak terkendali. Episode ini membangun fondasi serving yang benar dari request pertama sampai pola skala.
FastAPI adalah pilihan standar ekosistem Python ML: async-native, validasi otomatis, dokumentasi interaktif gratis di /docs.
from contextlib import asynccontextmanager
from pathlib import Path
import uvicorn
from fastapi import FastAPI, UploadFile, File
from ultralytics import YOLO
ml = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
ml["model"] = YOLO("best.pt") # load SEKALI saat startup
yield
ml.clear()
app = FastAPI(title="YOLO Detection API", lifespan=lifespan)
@app.post("/predict")
async def predict(file: UploadFile = File(...), conf: float = 0.25):
tmp = Path(f"/tmp/{file.filename}")
tmp.write_bytes(await file.read())
results = ml["model"].predict(source=str(tmp), conf=conf, verbose=False)[0]
return {
"detections": [
{
"class": results.names[int(b.cls.item())],
"confidence": round(float(b.conf.item()), 3),
"box_xyxy": [round(v) for v in b.xyxy[0].tolist()],
}
for b in results.boxes
]
}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)Uji cepatnya:
uvicorn main:app --host 0.0.0.0 --port 8000
curl -X POST http://localhost:8000/predict -F "file=@street.jpg"Dua keputusan krusial di kode itu yang sering salah pemula:
lifespan + load sekali — memuat model di dalam handler /predict berarti tiap request membayar 2–5 detik loading. Fatal.Warning
model.predict() Ultralytics bersifat blocking. Di FastAPI async, panggilan blocking di handler async bisa memblokir event loop dan membekukan seluruh service. Solusi pragmatis: jalankan handler dengan def (bukan async def) sehingga FastAPI otomatis memindahkannya ke threadpool — atau pakai run_in_executor untuk kontrol penuh.
| Pola | Cocok Untuk | Latency | Throughput | Kompleksitas |
|---|---|---|---|---|
| Realtime per-request | Upload user, integrasi app | Rendah | Sedang | Rendah |
| Batch | Analisis massal, backfill dataset | Tinggi per item | Tinggi | Sedang |
| Streaming | CCTV/live feed | Sangat rendah | Per-stream | Tinggi |
Untuk ribuan file sekaligus, HTTP per-file boros overhead. Pola yang benar: worker memproses dari antrian/storage:
from pathlib import Path
from ultralytics import YOLO
model = YOLO("best.pt")
files = sorted(Path("inbox").glob("*.jpg"))
results = model.predict( # list path = proses berkelompok
source=[str(f) for f in files], conf=0.35, stream=True,
)
for src, r in zip(files, results):
n = len(r.boxes)
print(f"{src.name}: {n} objek")
src.rename(Path("processed") / src.name) # pindah agar tak diproses ulangBatch memberi dua bonus tersembunyi: util GPU lebih efisien (beberapa gambar per forward pass), dan logika retry gampang — file gagal tinggal ditinggal di folder inbox.
Sudah kita dasari di episode 4 dan 16 — loop stream=True di atas source RTSP/file. Versi server-nya: satu worker per stream, hasil deteksi dikirim sebagai event (WebSocket/MQTT), frame mentah tidak pernah keluar host. Jangan pernah memproses N stream dalam satu process Python — GIL akan menjadi bottleneck; jalankan N process/worker.
Ketika satu instance tak lagi cukup:
Prinsip-prinsipnya:
Tip
Sebelum menambah kompleksitas, ukur dulu: satu instance yolo26n di GPU T4 mampu puluhan prediksi/detik resolusi 640. Mayoritas project gagal skala justru di tahap salah arsitektur (load model per-request, sync di async handler) — bukan karena kurang GPU.
[x] Model dimuat sekali di startup (lifespan/init container)
[x] Handler tidak memblokir event loop async
[x] Batas ukuran file & rate limit di endpoint publik
[x] Health check (/healthz) mencakup readiness model
[x] Log latensi terpisah: preprocess vs inference vs postprocess
[x] Rencana rollback versi model (artifact versioned, ep18)Rangkuman episode ini:
Di episode 21 kita kejar performa terakhir: optimasi inference — half precision FP16, quantization INT8, dan pruning — lengkap dengan protokol benchmark akurasi-vs-kecepatan supaya optimasi tidak menjadi sabotase senyap. Sampai jumpa!