P95 latensi yang melampaui SLA tidak dipulihkan dengan menebak-nebak. Langkah pertama optimasi selalu sama: membedah dari mana waktu itu sebenarnya datang, karena tindakan yang tepat untuk preprocessing yang lambat berbeda dari tindakan untuk model yang lambat. Diagram berikut memecah jalur satu request menjadi tiga sumber utama beserta contoh perbaikannya:
Teknik pada kotak model terbagi dua keluarga: yang mengubah cara menjalankan graph tanpa mengubah hasilnya (thread, graph optimization, caching), dan yang mengubah angka di dalam model demi kecepatan (quantization dan pruning — dibahas di chapter berikutnya). Chapter ini mempraktikkan keluarga pertama di mesin uji dua core: pengaturan thread untuk PyTorch dan onnxruntime, serta graph optimization yang diverifikasi menjaga output tetap identik.
Pengaturan Thread: Pengungkit Paling Murah #
Kedua runtime membagi pekerjaan matrix multiplication ke sejumlah thread intra-op. Menambah thread mempercepat komputasi besar, tetapi membayar biaya koordinasi — pada mesin dua core, ekspektasi yang realistis adalah sekitar 1,5 kali lipat, bukan dua kali lipat. Aturan praktisnya: jumlah thread yang menambah nilai adalah jumlah core yang benar-benar tersedia untuk proses ini; melebihi itu justru menambah kontensi. Runtime kadang membaca jumlah core lingkungan secara berbeda dari asumsi kita, sehingga menyetel eksplisit (torch.set_num_threads, intra_op_num_threads) adalah tindakan deterministik yang sehat.
Praktik: Thread dan Graph Optimization yang Terukur #
Berkas optimasi_latensi.py mengukur model menengah (512-512-512-256, batch 64) dalam empat pengukuran: PyTorch satu lawan dua thread, onnxruntime satu lawan dua thread, lalu verifikasi output graph optimization:
"""Optimasi latensi: pengaturan thread CPU dan graph optimization.
Alur: siapkan model menengah -> ukur PyTorch dengan 1 thread vs
2 thread (mesin uji 2 core) -> export ke ONNX -> ukur onnxruntime
dengan intra_op_num_threads 1 vs 2 -> verifikasi graph optimization
mempertahankan output identik.
"""
import time
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn
torch.manual_seed(42)
model = nn.Sequential(
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 512),
nn.ReLU(),
nn.Linear(512, 256),
)
model.eval()
batch = torch.randn(64, 512)
def ukur(label, fungsi, jumlah_iterasi=30, jumlah_putaran=3):
"""Warm-up lalu ukur beberapa putaran; laporkan yang tercepat."""
for _ in range(10):
fungsi()
terbaik = float("inf")
for _ in range(jumlah_putaran):
mulai = time.perf_counter()
for _ in range(jumlah_iterasi):
fungsi()
terbaik = min(terbaik, (time.perf_counter() - mulai) / jumlah_iterasi)
print(f"{label:38s} {terbaik * 1000:8.3f} ms/iterasi")
return terbaik
# ---------- 1. PyTorch: jumlah thread intra-op ----------
with torch.no_grad():
torch.set_num_threads(1)
t_satu = ukur("PyTorch set_num_threads(1)", lambda: model(batch))
torch.set_num_threads(2)
t_dua = ukur("PyTorch set_num_threads(2)", lambda: model(batch))
print(f"-> 2 thread {t_satu / t_dua:.2f}x lebih cepat dari 1 thread\n")
# ---------- 2. Export ke ONNX ----------
torch.onnx.export(
model,
(batch,),
"model_opt.onnx",
input_names=["x"],
output_names=["y"],
verbose=False,
)
import onnxruntime as ort
x_numpy = batch.numpy()
def buat_sesi(jumlah_thread, level_opt):
opsi = ort.SessionOptions()
opsi.intra_op_num_threads = jumlah_thread
opsi.graph_optimization_level = level_opt
return ort.InferenceSession(
"model_opt.onnx", opsi, providers=["CPUExecutionProvider"]
)
# ---------- 3. onnxruntime: jumlah thread intra-op ----------
sesi_satu = buat_sesi(1, ort.GraphOptimizationLevel.ORT_ENABLE_ALL)
sesi_dua = buat_sesi(2, ort.GraphOptimizationLevel.ORT_ENABLE_ALL)
o_satu = ukur("ORT intra_op_num_threads=1",
lambda: sesi_satu.run(None, {"x": x_numpy}))
o_dua = ukur("ORT intra_op_num_threads=2",
lambda: sesi_dua.run(None, {"x": x_numpy}))
print(f"-> 2 thread {o_satu / o_dua:.2f}x lebih cepat dari 1 thread\n")
# ---------- 4. Graph optimization: output harus tetap identik ----------
sesi_tanpa = buat_sesi(2, ort.GraphOptimizationLevel.ORT_DISABLE_ALL)
sesi_penuh = buat_sesi(2, ort.GraphOptimizationLevel.ORT_ENABLE_ALL)
y_tanpa = sesi_tanpa.run(None, {"x": x_numpy})[0]
y_penuh = sesi_penuh.run(None, {"x": x_numpy})[0]
print("graph opt DISABLE_ALL vs ENABLE_ALL:")
print(" output identik :", np.array_equal(y_tanpa, y_penuh))
print(" selisih maksimum:", f"{np.abs(y_tanpa - y_penuh).max():.2e}")
Path("model_opt.onnx").unlink()
print("berkas model_opt.onnx dihapus dari bundle")
print("catatan: angka waktu bervariasi antar eksekusi.")Output yang Dihasilkan:
PyTorch set_num_threads(1) 0.735 ms/iterasi
PyTorch set_num_threads(2) 0.484 ms/iterasi
-> 2 thread 1.52x lebih cepat dari 1 thread
ORT intra_op_num_threads=1 0.590 ms/iterasi
ORT intra_op_num_threads=2 0.381 ms/iterasi
-> 2 thread 1.55x lebih cepat dari 1 thread
graph opt DISABLE_ALL vs ENABLE_ALL:
output identik : True
selisih maksimum: 0.00e+00
berkas model_opt.onnx dihapus dari bundle
catatan: angka waktu bervariasi antar eksekusi.Mari kita telaah kode di atas baris per baris:
- Baris 17-24 (model dan batch): model sengaja berukuran menengah — matriks 512×512 dengan batch 64 menghasilkan sekitar 33 juta operasi kali-tambah per forward, cukup besar agar perbedaan thread terukur dan stabil, cukup kecil agar seluruh pengukuran selesai dalam hitungan detik.
- Baris 27-40 (
ukur): setiap konfigurasi mendapat warm-up 10 iterasi lalu tiga putaran pengukuran; yang dilaporkan adalah putaran tercepat. Filter “nilai minimum” menghapus lonjakan beban sesaat dari sistem operasi yang tidak mencerminkan kemampuan konfigurasi. - Baris 43-52 (export): model yang sama diekspor sekali ke ONNX (materi chapter ONNX dan Model Export ) agar pengukuran onnxruntime memakai graph yang identik dengan versi PyTorch-nya.
- Baris 61-70 (dua session ORT): pengungkit thread di onnxruntime adalah
intra_op_num_threadspadaSessionOptions— padanan langsungtorch.set_num_threads. Hasilnya paralel dengan bagian PyTorch: 1,55 kali lipat. - Baris 73-80 (verifikasi graph opt): dua session dengan level optimasi berbeda dijalankan pada input yang sama.
np.array_equalmengembalikanTrue— graph optimization menyusun ulang graph tanpa mengubah hasil, sehingga aman diaktifkan secara bawaan.
Pada model ini selisih waktu antar level graph optimization nyaris tidak terlihat karena graph-nya memang hanya terdiri dari beberapa operasi Gemm dan Relu yang sudah efisien. Manfaatnya muncul pada graph dengan banyak operasi kecil beruntun — khas model transformer — ketika fusion menggabungkan puluhan operasi element-wise menjadi satu. Panduan resminya, termasuk daftar jenis optimasi per level, ada di onnxruntime.ai/docs/performance/model-optimizations/graph-optimizations dan panduan threading di onnxruntime.ai/docs/performance/tune_perf/threading .
Peta Teknik dan Upayanya #
| Teknik | Hemat yang realistis | Upaya | Catatan |
|---|---|---|---|
| Set thread sesuai core | 1,3-1,6x pada 2 core | sangat rendah | ukur dulu; jangan oversubscribe |
| Graph optimization | 0-30%, bergantung graph | sangat rendah | bawaan ORT; output tetap identik |
| Caching hasil/preprocessing | besar bila input berulang | rendah | hati-hati keusangan cache |
| Kurangi titik sinkronisasi | bergantung arsitektur | sedang | batch async sebelum kumpulkan |
| Operator fusion manual | bergantung model | tinggi | umumnya serahkan ke runtime |
Dua pengungkit hari ini menjalankan graph apa adanya. Keluarga optimasi berikutnya berani mengubah angka di dalam model sendiri: bobot direpresentasikan lebih ringkas (quantization) dan bobot yang nyaris tak berperan dibuang (pruning) — dengan konsekuensi akurasi yang harus diukur, bukan diasumsikan.