GPU menawarkan ribuan core yang bekerja serentak — tetapi menempatkan model di GPU bukan jaminan otomatis lebih cepat. Antara request pengguna dan hasil prediksi terdapat anatomi yang harus dipahami: context CUDA yang harus dihidupkan, data yang menyeberang bus PCIe dari RAM ke memori GPU, dan kernel yang baru efisien saat ada cukup pekerjaan paralel untuk mengisi seluruh core. Chapter ini membedah anatomi itu satu per satu, menjelaskan mengapa GPU unggul pada throughput massal namun bisa kalah dari CPU untuk request tunggal, lalu mempraktikkan pola kode agnostik-perangkat yang benar.
CUDA Context dan Transfer Memori #
Tiga peristiwa menentukan bentuk latensi GPU. Pertama, CUDA context: proses pertama yang menyentuh GPU memuat driver dan mengalokasikan sumber daya — sekali saja, dengan biaya sekian detik, dan menjadi alasan server inference memuat model saat penyalaan (warm-up), bukan saat request pertama tiba. Kedua, transfer H2D/D2H: input harus menyeberang dari memori sistem ke memori GPU (host-to-device), hasil mengembalikan arah sebaliknya (device-to-host). Bus PCIe generasi 3 x16 yang umum di server memiliki bandwidth teoretis sekitar 16 GB/s dan efektif lebih rendah — tensor 4 MB memakan sekitar seperempat milidetik sekali jalan, dan perpindahan ini terjadi pada setiap request yang datang dari CPU. Ketiga, eksekusi kernel: inilah bagian yang benar-benar dipercepat GPU.
Alur lengkapnya:
Implikasinya tegas: model kecil dengan input kecil tidak punya komputasi cukup untuk menutupi biaya dua transfer plus inisiasi kernel. Semakin besar model dan batch-nya, semakin kecil porsi transfer dalam total waktu — hingga pada titik tertentu GPU menjadi satu-satunya pilihan yang masuk akal.
Batching GPU, TF32, dan Multi-GPU #
Batching adalah cara mengisi GPU: request-request yang berdekatan waktu digabung menjadi satu batch besar sehingga transfer dan inisiasi kernel dibayar sekali untuk semua anggota batch. TF32 (di kartu Ampere ke atas) memformat perkalian tensor dalam mantissa 10-bit untuk menggandakan throughput tanpa perubahan kode; AMP inference (fp16/bf16) menempuh jalur yang sama lebih jauh — keluarga tekniknya sudah kita bahas pada Mixed Precision Training . Multi-GPU serving menambah dimensi lagi: beberapa kartu berbagi beban request (replica), atau satu model terpecah di beberapa kartu untuk model yang tidak muat di satu memori.
Mengapa GPU mahal untuk batch 1? Kartu dengan ribuan core menghitung secara paralel — batch 1 hanya mengisi sebagian kecil core sementara sisanya menganggur, padahal biaya transfer H2D dan inisiasi kernel tetap dibayar penuh. Throughput puncak GPU diukur pada batch besar; latensi batch-1 sering kalah dari CPU untuk model kecil.
| Skenario | CPU (2-16 core) | GPU (ribuan core) |
|---|---|---|
| Batch 1, model kecil | sering lebih cepat | transfer + context membebani |
| Batch besar, model besar | lambat | menang telak |
| Throughput (prediksi/detik) | ratusan-ribuan | puluhan ribu-jutaan |
| Biaya per prediksi massal | naik linear dengan beban | turun saat batch membesar |
| Biaya operasional | rendah | tinggi (harga, daya, pendinginan) |
Pola Kode Agnostik-Perangkat #
Kode yang sehat menulis satu jalur untuk semua perangkat: deteksi ketersediaan, pindahkan model sekali di awal, pindahkan setiap input, sinkronkan sebelum membaca waktu. Berkas pola_perangkat.py menerapkan pola itu dan diuji nyata di mesin CPU ini — jalur cuda hanya akan aktif di mesin ber-GPU tanpa mengubah satu baris pun:
"""Pola kode agnostik-perangkat: berjalan di CUDA bila ada, di CPU bila tidak.
Skrip ini diuji nyata pada mesin CPU (tanpa CUDA), sehingga jalur CPU
yang tereksekusi. Jalur CUDA dari pola yang sama berlaku tanpa
perubahan kode pada mesin yang memiliki GPU. Alur: deteksi perangkat
-> pindahkan model dan tensor -> inference -> verifikasi hasil sama
dengan versi CPU.
"""
import time
import torch
import torch.nn as nn
torch.manual_seed(42)
model_cpu = nn.Sequential(
nn.Linear(64, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 4),
)
model_cpu.eval()
# ---------- 1. Deteksi perangkat ----------
cuda_tersedia = torch.cuda.is_available()
print(f"torch.cuda.is_available(): {cuda_tersedia}")
perangkat = torch.device("cuda" if cuda_tersedia else "cpu")
print(f"perangkat terpilih : {perangkat.type}")
# ---------- 2. Pindahkan model dan data ke perangkat ----------
model = model_cpu.to(perangkat)
torch.manual_seed(7)
input_cpu = torch.randn(8, 64)
input_perangkat = input_cpu.to(perangkat)
# ---------- 3. Inference di perangkat terpilih ----------
with torch.no_grad():
mulai = time.perf_counter()
output = model(input_perangkat)
if perangkat.type == "cuda":
torch.cuda.synchronize() # tunggu kernel GPU selesai sebelum membaca waktu
durasi = (time.perf_counter() - mulai) * 1000
print(f"waktu inference batch 8 : {durasi:.3f} ms (bervariasi antar eksekusi)")
# ---------- 4. Verifikasi hasil sama dengan referensi CPU ----------
with torch.no_grad():
referensi = model_cpu(input_cpu)
print(f"hasil sama dengan CPU : {torch.allclose(output.cpu(), referensi)}")
print(f"nama perangkat : {perangkat}")Output yang Dihasilkan:
torch.cuda.is_available(): False
perangkat terpilih : cpu
waktu inference batch 8 : 1.135 ms (bervariasi antar eksekusi)
hasil sama dengan CPU : True
nama perangkat : cpuMari kita telaah kode di atas baris per baris:
- Baris 27-30 (deteksi): satu titik keputusan —
torch.device("cuda" if ... else "cpu")— menggantikan percabanganifyang tersebar di seluruh kode. Referensi resminya di docs.pytorch.org/docs/stable/notes/cuda.html . - Baris 35-38 (pemindahan): model dipindahkan sekali saat proses menyala; input dipindahkan per request. Menaruh
.to(perangkat)di dalam loop input adalah kesalahan klasik yang membuang-buang waktu untuk objek yang sama. - Baris 41-47 (
torch.cuda.synchronize): panggilan GPU bersifat asinkron — kode Python melanjutkan sebelum kernel selesai. Tanpasynchronize(), jam mengukur waktu antrean, bukan waktu komputasi; pengukuran latensi GPU yang jujur selalu disertai sinkronisasi. Di jalur CPU cabang ini tidak dieksekusi. - Baris 50-53 (verifikasi): hasil dari perangkat dipanggil balik dengan
output.cpu()lalu dibandingkan dengan referensi CPU — langkah pencegahan saat memindahkan beban antar perangkat, karena aritmetika fp16 di GPU dapat menggeser hasil beberapa digit terakhir.
Untuk mesin ber-GPU, pola pengukuran transfer H2D/D2H menempuh bentuk berikut (ditampilkan sebagai pola; tidak dieksekusi di mesin CPU ini):
def ukur_transfer_gpu(tensor_cpu, jumlah_pengulangan=100):
"""Pola pengukur transfer H2D/D2H dan inference di mesin ber-GPU."""
import time
perangkat = torch.device("cuda")
tensor_gpu = tensor_cpu.to(perangkat)
mulai = time.perf_counter()
for _ in range(jumlah_pengulangan):
x = tensor_cpu.to(perangkat) # H2D: RAM -> memori GPU
torch.cuda.synchronize()
durasi_h2d = (time.perf_counter() - mulai) / jumlah_pengulangan
mulai = time.perf_counter()
for _ in range(jumlah_pengulangan):
y = tensor_gpu.to("cpu") # D2H: memori GPU -> RAM
durasi_d2h = (time.perf_counter() - mulai) / jumlah_pengulangan
return durasi_h2d, durasi_d2hPerangkat menjalankan model; tetapi sistem yang sehat juga harus mengawasi modelnya setelah berjalan — latensi yang merayap naik, error yang muncul sesekali, dan distribusi prediksi yang bergeser diam-diam. Chapter penutup section ini membangun dasar pemantauan tersebut.