Tutorial 19: Triton Inference Server - Penyajian Model Berperforma Tinggi
Daftar Isi
Pendahuluan
NVIDIA Triton Inference Server adalah perangkat lunak serving inferensi open-source yang memungkinkan tim men-deploy model AI dari framework apa pun pada infrastruktur GPU atau CPU. Server ini mendukung berbagai format model secara bersamaan — TensorFlow, PyTorch, ONNX Runtime, TensorRT, dan backend Python kustom — semuanya dari satu instance server tunggal.
Triton menangani masalah-masalah sulit dalam penyajian model produksi: dynamic batching untuk memaksimalkan pemanfaatan GPU, ensemble model untuk pipeline multi-tahap, eksekusi model secara bersamaan, versioning model, pemantauan kesehatan, dan ekspor metrik. Tutorial ini membahas semuanya mulai dari pengaturan awal hingga deploy Kubernetes tingkat produksi.
Prasyarat
- Docker terinstal dengan NVIDIA Container Toolkit
- GPU NVIDIA dengan CUDA 12.0+ (untuk serving GPU)
- Python 3.9+ dengan library tritonclient
- Pemahaman dasar konsep model serving
- kubectl dan helm (untuk bagian Kubernetes)
# Instal library klien Triton
pip install tritonclient[all] grpcio numpy
import tritonclient.grpc as grpcclient
import tritonclient.http as httpclient
import numpy as np
Menyiapkan Triton Inference Server
Mengunduh dan Menjalankan Triton
# Unduh kontainer Triton Inference Server
docker pull nvcr.io/nvidia/tritonserver:24.01-py3
Buat direktori repository model
mkdir -p /opt/triton/repositorymodel
Jalankan Triton dengan dukungan GPU
docker run --gpus all \
--rm -p 8000:8000 -p 8001:8001 -p 8002:8002 \
-v /opt/triton/repositorymodel:/models \
nvcr.io/nvidia/tritonserver:24.01-py3 \
tritonserver --model-repository=/models \
--log-verbose=1
Port:
8000 - Endpoint HTTP/REST
8001 - Endpoint gRPC
8002 - Endpoint Metrik (format Prometheus)
Memverifikasi Server
import tritonclient.http as httpclient
Periksa kesehatan server
client = httpclient.InferenceServerClient(url="localhost:8000")
print(f"Server hidup: {client.isserverlive()}")
print(f"Server siap: {client.isserverready()}")
Daftar model yang dimuat
indeksrepository = client.getmodelrepositoryindex()
for model in indeksrepository:
print(f"Model: {model['name']}, Versi: {model.get('version', 'N/A')}, "
f"Status: {model.get('state', 'N/A')}")
Struktur Repository Model
Triton menggunakan struktur direktori tertentu untuk repository modelnya. Setiap model memiliki direktorinya sendiri dengan file konfigurasi dan subdirektori versi bernomor.
repositorymodel/
├── pengklasifikasigambar/
│ ├── config.pbtxt
│ ├── 1/
│ │ └── model.onnx
│ └── 2/
│ └── model.onnx
├── encoderteks/
│ ├── config.pbtxt
│ └── 1/
│ └── model.pt
├── ekstraktorfitur/
│ ├── config.pbtxt
│ └── 1/
│ └── model.savedmodel/
│ ├── savedmodel.pb
│ └── variables/
└── praprosesor/
├── config.pbtxt
└── 1/
└── model.py
Konfigurasi Model (config.pbtxt)
# config.pbtxt untuk pengklasifikasi gambar ONNX
name: "pengklasifikasigambar"
platform: "onnxruntimeonnx"
maxbatchsize: 32
input [
{
name: "input"
datatype: TYPEFP32
dims: [ 3, 224, 224 ]
}
]
output [
{
name: "output"
datatype: TYPEFP32
dims: [ 1000 ]
labelfilename: "labels.txt"
}
]
Kebijakan versi: sajikan hanya versi terbaru
versionpolicy: { latest { numversions: 1 }}
Konfigurasi instance
instancegroup [
{
count: 2
kind: KINDGPU
gpus: [ 0 ]
}
]
Dukungan Multiple Backend
Backend PyTorch (TorchScript)
# Ekspor model PyTorch untuk Triton
import torch
import torchvision.models as models
model = models.resnet50(pretrained=True)
model.eval()
Buat model TorchScript
modelscripted = torch.jit.script(model)
modelscripted.save("repositorymodel/resnet50pytorch/1/model.pt")
# config.pbtxt untuk model PyTorch
name: "resnet50pytorch"
platform: "pytorchlibtorch"
maxbatchsize: 64
input [
{
name: "INPUT0"
datatype: TYPEFP32
dims: [ 3, 224, 224 ]
}
]
output [
{
name: "OUTPUT0"
datatype: TYPEFP32
dims: [ 1000 ]
}
]
Backend TensorFlow (SavedModel)
# config.pbtxt untuk TensorFlow SavedModel
name: "pengklasifikasitekstf"
platform: "tensorflowsavedmodel"
maxbatchsize: 128
input [
{
name: "inputids"
datatype: TYPEINT32
dims: [ 512 ]
},
{
name: "attentionmask"
datatype: TYPEINT32
dims: [ 512 ]
}
]
output [
{
name: "logits"
datatype: TYPEFP32
dims: [ 3 ]
}
]
Backend ONNX Runtime
# Konversi model ke ONNX untuk Triton
import torch
import onnx
model = models.resnet50(pretrained=True)
model.eval()
inputdummy = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
inputdummy,
"repositorymodel/resnet50onnx/1/model.onnx",
inputnames=['input'],
outputnames=['output'],
dynamicaxes={
'input': {0: 'batchsize'},
'output': {0: 'batchsize'}
},
opsetversion=17
)
Verifikasi model ONNX
modelonnx = onnx.load("repositorymodel/resnet50onnx/1/model.onnx")
onnx.checker.checkmodel(modelonnx)
print("Model ONNX valid")
Backend Python (Logika Kustom)
# repositorymodel/praprosesor/1/model.py
import tritonpythonbackendutils as pbutils
import numpy as np
from PIL import Image
import io
import json
class TritonPythonModel:
"""Backend Python kustom untuk praproses gambar."""
def initialize(self, args):
"""Dipanggil sekali saat model dimuat."""
self.modelconfig = json.loads(args['modelconfig'])
self.outputdtype = pbutils.tritonstringtonumpy(
pbutils.getoutputconfigbyname(
self.modelconfig, "gambarterproses"
)['datatype']
)
# Normalisasi ImageNet standar
self.mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
self.std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
def execute(self, requests):
"""Proses setiap permintaan inferensi."""
responses = []
for request in requests:
# Dapatkan byte gambar mentah
gambarmentah = pbutils.getinputtensorbyname(
request, "gambarmentah"
).asnumpy()
# Decode dan praproses
gambar = Image.open(io.BytesIO(gambarmentah.tobytes()))
gambar = gambar.resize((224, 224))
gambar = np.array(gambar, dtype=np.float32) / 255.0
# Normalisasi
gambar = (gambar - self.mean) / self.std
# Transpose ke format CHW
gambar = np.transpose(gambar, (2, 0, 1))
gambar = np.expanddims(gambar, axis=0)
# Buat tensor output
tensoroutput = pbutils.Tensor(
"gambarterproses",
gambar.astype(self.outputdtype)
)
responses.append(
pbutils.InferenceResponse(
outputtensors=[tensoroutput]
)
)
return responses
def finalize(self):
"""Dipanggil sekali saat model di-unload."""
print("Model praprosesor selesai")
# config.pbtxt untuk backend Python
name: "praprosesor"
backend: "python"
maxbatchsize: 0
input [
{
name: "gambarmentah"
datatype: TYPEUINT8
dims: [ -1 ]
}
]
output [
{
name: "gambarterproses"
datatype: TYPEFP32
dims: [ 1, 3, 224, 224 ]
}
]
instancegroup [
{
count: 2
kind: KINDCPU
}
]
Dynamic Batching
Dynamic batching memungkinkan Triton menggabungkan permintaan inferensi individual menjadi satu batch, memaksimalkan throughput GPU tanpa mengharuskan klien untuk melakukan batching secara manual.
# config.pbtxt dengan dynamic batching
name: "pengklasifikasigambar"
platform: "onnxruntimeonnx"
maxbatchsize: 64
input [
{
name: "input"
datatype: TYPEFP32
dims: [ 3, 224, 224 ]
}
]
output [
{
name: "output"
datatype: TYPEFP32
dims: [ 1000 ]
}
]
dynamicbatching {
preferredbatchsize: [ 8, 16, 32 ]
maxqueuedelaymicroseconds: 100
# Level prioritas untuk tipe permintaan berbeda
prioritylevels: 3
defaultprioritylevel: 2
# Kebijakan antrian
defaultqueuepolicy {
timeoutaction: REJECT
defaulttimeoutmicroseconds: 5000000
allowtimeoutoverride: true
maxqueuesize: 100
}
}
Batching Sisi Klien
import tritonclient.grpc as grpcclient
import numpy as np
from concurrent.futures import ThreadPoolExecutor
import time
def kirimpermintaaninferensi(client, namamodel, datagambar):
"""Kirim satu permintaan inferensi."""
inputs = [
grpcclient.InferInput("input", datagambar.shape, "FP32")
]
inputs[0].setdatafromnumpy(datagambar)
outputs = [
grpcclient.InferRequestedOutput("output")
]
hasil = client.infer(
modelname=namamodel,
inputs=inputs,
outputs=outputs
)
return hasil.asnumpy("output")
Kirim permintaan bersamaan (Triton mem-batch secara otomatis)
client = grpcclient.InferenceServerClient(url="localhost:8001")
namamodel = "pengklasifikasigambar"
Simulasikan 100 permintaan bersamaan
gambarlist = [
np.random.rand(1, 3, 224, 224).astype(np.float32)
for in range(100)
]
waktumulai = time.time()
with ThreadPoolExecutor(maxworkers=16) as executor:
futures = [
executor.submit(kirimpermintaaninferensi, client, namamodel, img)
for img in gambarlist
]
hasillist = [f.result() for f in futures]
waktuberlalu = time.time() - waktumulai
print(f"Memproses {len(gambarlist)} gambar dalam {waktuberlalu:.2f} detik")
print(f"Throughput: {len(gambarlist) / waktuberlalu:.1f} gambar/detik")
Pipeline Ensemble Model
Ensemble model merantai beberapa model dalam graf asiklik terarah (DAG), dengan Triton mengelola aliran data di antara model-model tersebut.
# config.pbtxt untuk model ensemble
name: "pipelinegambar"
platform: "ensemble"
maxbatchsize: 32
input [
{
name: "gambarmentah"
datatype: TYPEUINT8
dims: [ -1 ]
}
]
output [
{
name: "klasifikasi"
datatype: TYPEFP32
dims: [ 1000 ]
}
]
ensemblescheduling {
step [
{
modelname: "praprosesor"
modelversion: -1
inputmap {
key: "gambarmentah"
value: "gambarmentah"
}
outputmap {
key: "gambarterproses"
value: "outputterproses"
}
},
{
modelname: "pengklasifikasigambar"
modelversion: -1
inputmap {
key: "input"
value: "outputterproses"
}
outputmap {
key: "output"
value: "klasifikasi"
}
}
]
}
Contoh Ensemble Multi-Model
# Ensemble lebih kompleks: praproses -> ekstrak fitur -> klasifikasi + deteksi
name: "pipelinemultitugas"
platform: "ensemble"
maxbatchsize: 16
input [
{
name: "gambarmentah"
datatype: TYPEUINT8
dims: [ -1 ]
}
]
output [
{
name: "labelkelas"
datatype: TYPEFP32
dims: [ 1000 ]
},
{
name: "kotakbatas"
datatype: TYPEFP32
dims: [ 100, 6 ]
}
]
ensemblescheduling {
step [
{
modelname: "praprosesor"
modelversion: -1
inputmap { key: "gambarmentah" value: "gambarmentah" }
outputmap { key: "gambarterproses" value: "terproses" }
},
{
modelname: "ekstraktorfitur"
modelversion: -1
inputmap { key: "input" value: "terproses" }
outputmap { key: "fitur" value: "fiturbersama" }
},
{
modelname: "kepalaklasifikasi"
modelversion: -1
inputmap { key: "fitur" value: "fiturbersama" }
outputmap { key: "logits" value: "labelkelas" }
},
{
modelname: "kepaladeteksi"
modelversion: -1
inputmap { key: "fitur" value: "fiturbersama" }
outputmap { key: "kotak" value: "kotakbatas" }
}
]
}
Penjadwalan GPU dan Manajemen Sumber Daya
Multiple Instance Model
# Jalankan beberapa instance di berbagai GPU
name: "modelbesar"
platform: "pytorchlibtorch"
maxbatchsize: 8
instancegroup [
{
# 2 instance di GPU 0
count: 2
kind: KINDGPU
gpus: [ 0 ]
},
{
# 1 instance di GPU 1
count: 1
kind: KINDGPU
gpus: [ 1 ]
},
{
# 1 instance CPU cadangan
count: 1
kind: KINDCPU
}
]
Pembatasan rate untuk mencegah kehabisan memori GPU
ratelimiter {
resources [
{
name: "memorigpu"
global: true
count: 4
}
]
}
Pemanasan Model (Warm-Up)
# Panaskan model untuk menghindari latensi cold-start
name: "pengklasifikasigambar"
platform: "onnxruntimeonnx"
maxbatchsize: 32
modelwarmup [
{
name: "pemanasanbatch1"
batchsize: 1
inputs {
key: "input"
value: {
datatype: TYPEFP32
dims: [ 3, 224, 224 ]
randomdata: true
}
}
},
{
name: "pemanasanbatch32"
batchsize: 32
inputs {
key: "input"
value: {
datatype: TYPEFP32
dims: [ 3, 224, 224 ]
randomdata: true
}
}
}
]
Performance Analyzer
Tool perfanalyzer milik Triton mengukur karakteristik throughput dan latensi model Anda di bawah berbagai kondisi beban.
# Instal perfanalyzer
Sudah termasuk dalam kontainer SDK Triton
docker pull nvcr.io/nvidia/tritonserver:24.01-py3-sdk
Tes throughput dasar
perfanalyzer -m pengklasifikasigambar \
-u localhost:8001 \
-i grpc \
--concurrency-range 1:16:2 \
--measurement-interval 10000
Analisis latensi detail
perfanalyzer -m pengklasifikasigambar \
-u localhost:8001 \
-i grpc \
--concurrency-range 1:32:4 \
--percentile=95 \
--measurement-interval 10000 \
-f hasil.csv
Pengujian Performa Secara Programatik
import tritonclient.grpc as grpcclient
import numpy as np
import time
from concurrent.futures import ThreadPoolExecutor, ascompleted
from dataclasses import dataclass
@dataclass
class HasilBenchmark:
totalpermintaan: int
totalwaktudetik: float
throughputrps: float
rata2latensims: float
p50latensims: float
p95latensims: float
p99latensims: float
jumlaherror: int
def benchmarkmodel(url, namamodel, bentukinput,
konkurensi=8, jumlahpermintaan=1000):
"""Jalankan benchmark komprehensif terhadap model Triton."""
client = grpcclient.InferenceServerClient(url=url)
daftarlatensi = []
error = 0
def permintaantunggal():
nonlocal error
try:
data = np.random.rand(bentukinput).astype(np.float32)
inputs = [grpcclient.InferInput("input", data.shape, "FP32")]
inputs[0].setdatafromnumpy(data)
outputs = [grpcclient.InferRequestedOutput("output")]
mulai = time.perfcounter()
client.infer(modelname=namamodel,
inputs=inputs, outputs=outputs)
berlalu = (time.perfcounter() - mulai) 1000
return berlalu
except Exception:
error += 1
return None
# Pemanasan
for in range(min(50, jumlahpermintaan // 10)):
permintaantunggal()
# Benchmark
waktumulai = time.time()
with ThreadPoolExecutor(maxworkers=konkurensi) as executor:
futures = [executor.submit(permintaantunggal)
for in range(jumlahpermintaan)]
for f in ascompleted(futures):
hasil = f.result()
if hasil is not None:
daftarlatensi.append(hasil)
totalwaktu = time.time() - waktumulai
arrlatensi = np.array(daftarlatensi)
return HasilBenchmark(
totalpermintaan=jumlahpermintaan,
totalwaktudetik=totalwaktu,
throughputrps=len(daftarlatensi) / totalwaktu,
rata2latensims=np.mean(arrlatensi),
p50latensims=np.percentile(arrlatensi, 50),
p95latensims=np.percentile(arrlatensi, 95),
p99latensims=np.percentile(arrlatensi, 99),
jumlaherror=error
)
Jalankan benchmark
hasil = benchmarkmodel(
url="localhost:8001",
namamodel="pengklasifikasigambar",
bentukinput=(1, 3, 224, 224),
konkurensi=16,
jumlahpermintaan=5000
)
print(f"Throughput: {hasil.throughputrps:.1f} req/detik")
print(f"Rata-rata latensi: {hasil.rata2latensims:.2f} ms")
print(f"P95 latensi: {hasil.p95latensims:.2f} ms")
print(f"P99 latensi: {hasil.p99latensims:.2f} ms")
print(f"Error: {hasil.jumlaherror}")
Deploy di Kubernetes
Deploy dengan Helm Chart
# triton-values.yaml - Nilai Helm chart untuk deploy Triton
replicaCount: 3
image:
repository: nvcr.io/nvidia/tritonserver
tag: "24.01-py3"
pullPolicy: IfNotPresent
resources:
limits:
nvidia.com/gpu: 1
memory: "16Gi"
cpu: "8"
requests:
nvidia.com/gpu: 1
memory: "8Gi"
cpu: "4"
service:
type: ClusterIP
httpPort: 8000
grpcPort: 8001
metricsPort: 8002
modelRepository:
storageType: "pvc"
pvc:
existingClaim: "triton-models-pvc"
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetGPUUtilization: 70
targetQueueLatency: 50 # ms
Manifest Kubernetes
# triton-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: triton-inference-server
labels:
app: triton
spec:
replicas: 3
selector:
matchLabels:
app: triton
template:
metadata:
labels:
app: triton
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8002"
spec:
containers:
- name: triton
image: nvcr.io/nvidia/tritonserver:24.01-py3
command: ["tritonserver"]
args:
- "--model-repository=/models"
- "--strict-model-config=false"
- "--log-verbose=0"
- "--exit-on-error=false"
- "--rate-limit=executioncount"
- "--rate-limit-resource=R1:4"
ports:
- containerPort: 8000
name: http
- containerPort: 8001
name: grpc
- containerPort: 8002
name: metrics
resources:
limits:
nvidia.com/gpu: 1
memory: "16Gi"
requests:
nvidia.com/gpu: 1
memory: "8Gi"
volumeMounts:
- name: repository-model
mountPath: /models
livenessProbe:
httpGet:
path: /v2/health/live
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /v2/health/ready
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
volumes:
- name: repository-model
persistentVolumeClaim:
claimName: triton-models-pvc
apiVersion: v1
kind: Service
metadata:
name: triton-service
spec:
selector:
app: triton
ports:
- name: http
port: 8000
targetPort: 8000
- name: grpc
port: 8001
targetPort: 8001
- name: metrics
port: 8002
targetPort: 8002
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: triton-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: triton-inference-server
minReplicas: 2
maxReplicas: 10
metrics:
- type: Pods
pods:
metric:
name: nvinferencequeuedurationus
target:
type: AverageValue
averageValue: "50000" # Target latensi antrian 50ms
Monitoring dengan Prometheus dan Grafana
# Triton mengekspos metrik Prometheus di :8002/metrics
Metrik penting yang perlu dipantau:
nvinferencerequestsuccess - jumlah inferensi berhasil
nvinferencerequestfailure - jumlah inferensi gagal
nvinferencecount - total jumlah inferensi
nvinferenceexeccount - jumlah eksekusi (batch)
nvinferencerequestdurationus - latensi permintaan
nvinferencequeuedurationus - waktu tunggu antrian
nvgpuutilization - persentase pemanfaatan GPU
nvgpumemoryusedbytes - penggunaan memori GPU
import requests
def ambilmetriktriton(urlmetrik="http://localhost:8002/metrics"):
"""Ambil dan parse metrik Prometheus Triton."""
response = requests.get(urlmetrik)
metrik = {}
for baris in response.text.split('\n'):
if baris and not baris.startswith('#'):
bagian = baris.split()
if len(bagian) == 2:
metrik[bagian[0]] = float(bagian[1])
return metrik
metrik = ambilmetriktriton()
for kunci, nilai in sorted(metrik.items()):
if 'nvinference' in kunci or 'nvgpu' in kunci:
print(f"{kunci}: {nilai}")
Praktik Terbaik
preferredbatchsize: [1], ini menangani antrian permintaan bersamaan dengan benar. Atur maxqueuedelaymicroseconds berdasarkan SLA latensi Anda.nvinferencequeueduration_us) sebagai metrik scaling utama. Jika waktu antrian meningkat, tambahkan lebih banyak instance model atau replika./v2/health/live untuk liveness probe dan /v2/health/ready untuk readiness probe.Kesimpulan
Triton Inference Server menyelesaikan kompleksitas operasional dalam menyajikan model ML di produksi. Dengan mendukung berbagai framework dari satu server tunggal, menyediakan dynamic batching secara bawaan, memungkinkan ensemble model untuk pipeline kompleks, dan terintegrasi secara alami dengan Kubernetes dan Prometheus, Triton memungkinkan tim engineering fokus pada kualitas model daripada infrastruktur. Performance analyzer memastikan Anda dapat menemukan dan memperbaiki bottleneck sebelum mencapai produksi, dan fitur penjadwalan GPU memaksimalkan pemanfaatan perangkat keras di seluruh armada model Anda.