Ketika pengguna mengetik kueri dan menunggu jawaban, kriteria suksesnya bukan throughput melainkan kecepatan satu jawaban. Real-time inference adalah jalur satu request dari input sampai keluar hasilnya, dan ukurannya bukan angka rata-rata — melainkan persentil: P50 untuk pengalaman tipikal, P95 dan P99 untuk pengalaman terburuk yang masih ditoleransi. Chapter ini menjalankan pengukuran sungguhan atas 100 request, menghitung ketiga persentil itu, lalu membedah mengapa ekor distribusi (tail latency) sering menunjuk ke sesuatu di luar model itu sendiri.
SLA Latensi dan Persentil #
Service level agreement (SLA) untuk layanan inference umumnya ditulis begini: “P95 latensi di bawah 100 milidetik”. Artinya: 95% seluruh request selesai di bawah 100 ms; 5% sisanya boleh lebih lambat. Mengapa tidak cukup rata-rata? Karena distribusi latensi jarang simetris — sedikit request yang sangat lambat dapat menarik rata-rata naik tanpa menggambarkan pengalaman siapa pun, atau sebaliknya menyembunyikan keluhan 5% pengguna yang paling lama menunggu di balik angka yang tampak baik.
| Metrik | Arti | Peran dalam SLA |
|---|---|---|
| P50 | separuh request lebih cepat dari nilai ini | pengalaman tipikal |
| P95 | 95% request lebih cepat dari nilai ini | janji layanan utama |
| P99 | 99% request lebih cepat dari nilai ini | batas kasus terburuk |
| rata-rata | luas rata-rata seluruh request | pemantau tren, bukan SLA |
| max | request paling lambat | diagnosis, bukan target |
Dua kebiasaan wajib sebelum mengukur. Pertama, warm-up: beberapa puluh request pertama selalu lebih lambat karena alokasi memori, kompilasi kernel, dan cache yang masih dingin — ukur setelah sistem panas. Kedua, ukur jalur penuh: dari momen request diterima sampai respons terkirim, bukan hanya model(x); lihat kembali diagram jalur serving pada chapter Model Serving
.
Praktik: Mengukur P50/P95/P99 dari 100 Request #
Berkas latensi_persentil.py menyiapkan model mini, menghangatkan dengan 20 request, lalu mengukur 100 request batch-1 satu per satu dengan time.perf_counter():
"""Real-time inference: mengukur latensi P50, P95, P99.
Alur: siapkan model mini -> warm-up 20 request -> ukur 100 request
batch-1 dengan perf_counter -> hitung P50/P95/P99 dengan metode
nearest-rank -> cetak tabel statistik.
"""
import time
import torch
import torch.nn as nn
torch.manual_seed(42)
model = nn.Sequential(
nn.Linear(16, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 2),
)
model.eval()
torch.manual_seed(7)
contoh_input = [torch.randn(1, 16) for _ in range(100)]
def persentil(data, persen):
"""Persentil metode nearest-rank: elemen ke-ceil(p/100 * n)."""
data_urut = sorted(data)
urutan = max(1, int(-(-len(data) * persen // 100))) # ceil tanpa import
return data_urut[urutan - 1]
# ---------- 1. Warm-up ----------
with torch.no_grad():
for x in contoh_input[:20]:
model(x)
# ---------- 2. Ukur 100 request ----------
latensi_ms = []
with torch.no_grad():
for x in contoh_input:
mulai = time.perf_counter()
model(x)
latensi_ms.append((time.perf_counter() - mulai) * 1000)
# ---------- 3. Statistik ----------
jumlah = len(latensi_ms)
print(f"jumlah request : {jumlah}")
print(f"{'metrik':>6} {'latensi (ms)':>14}")
print(f"{'min':>6} {min(latensi_ms):>14.3f}")
print(f"{'P50':>6} {persentil(latensi_ms, 50):>14.3f}")
print(f"{'P95':>6} {persentil(latensi_ms, 95):>14.3f}")
print(f"{'P99':>6} {persentil(latensi_ms, 99):>14.3f}")
print(f"{'max':>6} {max(latensi_ms):>14.3f}")
rata2 = sum(latensi_ms) / jumlah
print(f"{'rata2':>6} {rata2:>14.3f}")
print("\nlima request pertama (ms):",
[f"{v:.3f}" for v in latensi_ms[:5]])
print("catatan: nilai latensi bervariasi antar eksekusi;")
print("yang stabil adalah pola P50 < P95 < P99 <= max.")Output yang Dihasilkan:
jumlah request : 100
metrik latensi (ms)
min 0.023
P50 0.032
P95 7.833
P99 32.303
max 35.189
rata2 3.391
lima request pertama (ms): ['7.744', '7.777', '5.006', '7.703', '7.769']
catatan: nilai latensi bervariasi antar eksekusi;
yang stabil adalah pola P50 < P95 < P99 <= max.Mari kita telaah kode di atas baris per baris:
- Baris 18-25 (model dan input): 100 tensor input dibuat sekali di awal dengan seed tetap, sehingga hasil pengukuran tidak tercampur biaya pembuatan tensor di dalam jalur yang diukur.
- Baris 29-34 (
persentil): metode nearest-rank mengurutkan data lalu mengambil elemen ke-⌈p/100 × n⌉ — definisi persentil yang paling sederhana dan mudah diaudit. Fungsinumpy.percentilememakai interpolasi antar dua tetangga dan menjadi pilihan praktis untuk analisis nyata (numpy.org/doc/stable/reference/generated/numpy.percentile.html ). - Baris 37-39 (warm-up): 20 request pertama sengaja tidak diukur. Inilah sebabnya lima request pertama pada keluaran terlihat lambat (sekitar 7 ms) — sisa efek pemanasan pada awal pengukuran yang ikut terekam.
- Baris 42-48 (pengukuran):
perf_counter()dipasang rapat di sekelilingmodel(x)— jam resolusi tinggi yang tidak terpengaruh pergantian waktu sistem. Setiap request batch-1 diukur terpisah, meniru jalur real-time. - Baris 51-64 (laporan): min, P50, P95, P99, max, lalu rata-rata dicetak berdampingan agar perbedaannya terlihat sekali baca.
Hasil pengukuran ini membawa pelajaran yang tidak terduga: P50 hanya 0,032 ms — modelnya memang kecil — tetapi P95 melompat ke 7,8 ms dan P99 ke 32,3 ms. Lonjakan ekor ini bukan salah model, melainkan jejak penjadwalan sistem operasi pada mesin dua core: sesekali proses kita disela agar core diberikan kepada proses lain. Rata-rata 3,39 ms pun menipu — tidak ada satu pun request yang benar-benar memakan waktu 3,39 ms; angka itu hanyalah hasil campuran dua populasi. Inilah alasan SLA ditulis dalam P95/P99 dan alasan diagnosis ekor latensi menuntut melihat keseluruhan sistem, bukan hanya model.
Jalur Single Request dan Streaming #
Jalur real-time klasik adalah single request path: satu input, satu forward pass, satu jawaban utuh. Varian kedua yang makin umum adalah streaming: model menghasilkan jawaban bertahap dan setiap potongan dikirim segera — pengguna LLM melihat token demi token muncul, bukan menunggu paragraf penuh. Total waktunya bisa sama, tetapi time-to-first-token menjadi sangat singkat sehingga sistem terasa jauh lebih responsif. Teknik ini menuntut generator Python yang menghasilkan potongan output, protokol transport yang mendukung pengiriman bertahap (chunked HTTP, WebSocket), dan komposisi model yang memang dekoder autoregresif — dibahas dalam konteks LLM pada section Large Language Model.
Kita sekarang bisa mengukur latensi dengan jujur. Pertanyaan berikutnya: dari mana saja waktu itu sebenarnya bersumber, dan teknik apa yang bisa memangkasnya? Chapter berikutnya membedah sumber latensi satu per satu — dari preprocessing sampai pengaturan thread — dan mengujinya di mesin ini.