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.

SkenarioCPU (2-16 core)GPU (ribuan core)
Batch 1, model kecilsering lebih cepattransfer + context membebani
Batch besar, model besarlambatmenang telak
Throughput (prediksi/detik)ratusan-ribuanpuluhan ribu-jutaan
Biaya per prediksi massalnaik linear dengan bebanturun saat batch membesar
Biaya operasionalrendahtinggi (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:

python
"""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:

text
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             : cpu

Mari kita telaah kode di atas baris per baris:

  1. Baris 27-30 (deteksi): satu titik keputusan — torch.device("cuda" if ... else "cpu") — menggantikan percabangan if yang tersebar di seluruh kode. Referensi resminya di docs.pytorch.org/docs/stable/notes/cuda.html .
  2. 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.
  3. Baris 41-47 (torch.cuda.synchronize): panggilan GPU bersifat asinkron — kode Python melanjutkan sebelum kernel selesai. Tanpa synchronize(), jam mengukur waktu antrean, bukan waktu komputasi; pengukuran latensi GPU yang jujur selalu disertai sinkronisasi. Di jalur CPU cabang ini tidak dieksekusi.
  4. 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):

python
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_d2h

Perangkat 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.