GPU mendominasi percakapan soal deep learning, tetapi mayoritas inference dunia sesungguhnya berjalan di CPU: server tanpa akselerator, laptop, mesin kasir, hingga perangkat edge di pabrik. CPU tidak menawarkan kemenangan throughput mentah, melainkan hal-hal yang sering lebih menentukan: biaya rendah, latensi batch-kecil yang kompetitif, dan ketersediaan di hampir semua mesin. Chapter ini merangkum kapan CPU memadai, optimasi apa yang paling berbuah di CPU, dan metodologi benchmark yang jujur — semuanya diuji di mesin dua core yang menjadi lingkungan kerja kita.

Kapan CPU Sudah Cukup #

Keputusan CPU versus GPU bukan soal mana yang lebih cepat secara absolut, melainkan mana yang cocok dengan bentuk beban kerja:

Kondisi bebanPilihanAlasan
Latensi longgar (ratusan ms masih oke)CPUbiaya dan kompleksitas lebih rendah
Batch kecil (1-16), model kecil-sedangCPUbatch kecil menyia-nyiakan GPU
Throughput massal (batch ratusan, model besar)GPUparalelisme ribuan core
Perangkat edge tanpa GPUCPUsatu-satunya pilihan yang ada
Budget operasional ketatCPUGPU server mahal untuk disewa dan dijalankan

Titik beloknya mudah diingat: GPU unggul saat ada banyak pekerjaan paralel yang menumpuk. Request satu-satu atas model kecil justru sering lebih cepat di CPU karena tidak membayar biaya transfer data dan inisialisasi context GPU.

Optimasi Khas CPU #

Tiga pengungkit yang paling berbuah di CPU semuanya sudah kita miliki. Pertama, pengaturan thread sesuai jumlah core tersedia — diuji nyata pada chapter Optimasi Latensi dengan hasil 1,5 kali lipat di mesin dua core. Kedua, quantization int8 untuk memperkecil bobot dan mempercepat inference pada layer Linear besar — dampak ukurannya diukur pada Quantization dan Pruning . Ketiga, runtime ONNX yang menggantikan mesin eksekusi dengan graph yang sudah dioptimasi untuk CPU — dibangun pada ONNX dan Model Export .

Satu pengungkit tambahan yang khas CPU: onnxruntime menjalankan graph melalui provider — CPUExecutionProvider memakai kernel MLAS yang disetel khusus untuk arsitektur prosesor tertentu, lengkap dengan dukungan instruksi vektor (AVX) bila tersedia.

Praktik: Benchmark dengan Metodologi yang Benar #

Angka benchmark CPU hanya sebaik metodologinya. Tiga syarat minimum: warm-up agar alokasi memori dan cache tidak ikut terukur, banyak iterasi agar rata-rata bermakna, dan laporan rentang (minimum, P50, maksimum, simpangan baku) — bukan satu angka rata-rata yang menyembunyikan ekor. Berkas benchmark_cpu.py menerapkan ketiganya, sekaligus mengulang pengukuran untuk memperlihatkan varians antar putaran:

python
"""Benchmark inference CPU dengan metodologi yang benar.

Alur: siapkan model menengah -> untuk batch 1 dan batch 16: warm-up
20 iterasi, ukur 200 iterasi -> laporkan rata-rata, simpangan baku,
minimum, dan P50 -> ulangi pengukuran kedua untuk melihat varians
antar putaran.
"""

import statistics
import time

import torch
import torch.nn as nn

torch.manual_seed(42)
model = nn.Sequential(
    nn.Linear(64, 256),
    nn.ReLU(),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Linear(128, 4),
)
model.eval()

torch.manual_seed(7)
input_1 = torch.randn(1, 64)
input_16 = torch.randn(16, 64)


def benchmark(fungsi, warm_up=20, jumlah_iterasi=200):
    """Metodologi: warm-up -> N iterasi terukur -> statistik lengkap."""
    for _ in range(warm_up):
        fungsi()
    hasil = []
    for _ in range(jumlah_iterasi):
        mulai = time.perf_counter()
        fungsi()
        hasil.append((time.perf_counter() - mulai) * 1000)
    terurut = sorted(hasil)
    p50 = terurut[len(terurut) // 2]
    return {
        "rata2": statistics.mean(hasil),
        "std": statistics.stdev(hasil),
        "min": min(hasil),
        "p50": p50,
        "max": max(hasil),
    }


def laporan(label, fungsi):
    s = benchmark(fungsi)
    print(f"{label:10s} rata2 {s['rata2']:7.3f} ms | std {s['std']:7.3f} | "
          f"min {s['min']:7.3f} | P50 {s['p50']:7.3f} | max {s['max']:7.3f}")
    return s


print(f"CPU: {torch.get_num_threads()} thread PyTorch, 200 iterasi per ukur\n")
with torch.no_grad():
    a1 = laporan("batch 1", lambda: model(input_1))
    a16 = laporan("batch 16", lambda: model(input_16))
    print()
    b1 = laporan("batch 1", lambda: model(input_1))
    b16 = laporan("batch 16", lambda: model(input_16))

print(f"\nbatch 16 / batch 1 (putaran 1): {a16['rata2'] / a1['rata2']:.1f}x waktu,"
      f" {16 / (a16['rata2'] / a1['rata2']):.0f}x throughput per sampel")
print(f"ulangan rata2 batch 1 : {a1['rata2']:.3f} vs {b1['rata2']:.3f} ms")
print(f"ulangan rata2 batch 16: {a16['rata2']:.3f} vs {b16['rata2']:.3f} ms")
print("catatan: varians antar putaran normal pada benchmark CPU;")
print("laporkan rentang atau median dari beberapa putaran, bukan satu angka.")

Output yang Dihasilkan:

text
CPU: 2 thread PyTorch, 200 iterasi per ukur

batch 1    rata2   0.030 ms | std   0.011 | min   0.026 | P50   0.027 | max   0.163
batch 16   rata2   0.051 ms | std   0.005 | min   0.045 | P50   0.049 | max   0.070

batch 1    rata2   0.028 ms | std   0.004 | min   0.026 | P50   0.027 | max   0.047
batch 16   rata2   0.051 ms | std   0.007 | min   0.046 | P50   0.049 | max   0.096

batch 16 / batch 1 (putaran 1): 1.7x waktu, 9x throughput per sampel
ulangan rata2 batch 1 : 0.030 vs 0.028 ms
ulangan rata2 batch 16: 0.051 vs 0.051 ms
catatan: varians antar putaran normal pada benchmark CPU;
laporkan rentang atau median dari beberapa putaran, bukan satu angka.

Mari kita telaah kode di atas baris per baris:

  1. Baris 27-43 (benchmark): seluruh disiplin pengukuran berada di satu fungsi — warm-up 20, 200 iterasi terukur, lalu statistik lengkap. statistics.stdev mengukur sebaran; P50 diambil dari data terurut (median sederhana tanpa interpolasi).
  2. Baris 51-56 (dua putaran): pengukuran diulang agar pembaca melihat langsung varians antar putaran: rata-rata batch 1 bergeser 0,030 ke 0,028 ms, batch 16 stabil di 0,051 ms. Perbedaan kecil seperti ini wajar di CPU dua core — itulah mengapa laporan satu angka tunggal mudah menyesatkan.
  3. Baris 58-60 (rasio): batch 16 hanya memakan 1,7 kali waktu batch 1, sehingga throughput per sampel naik sembilan kali lipat — bukti nyata bahwa CPU pun mendapat manfaat batching, selaras temuan chapter Batch Inference .

Dokumentasi performa onnxruntime untuk CPU — termasuk pemilihan provider dan tautan ke panduan tuning — tersedia di onnxruntime.ai/docs ; resep resmi benchmark PyTorch, termasuk alat torch.utils.benchmark yang lebih canggih dari perf_counter, di docs.pytorch.org/docs/stable/benchmark_utils.html .

CPU memberi keandalan dan harga; GPU memberi tenaga pada skala. Untuk memahami kapan pindah, kita perlu melihat bagaimana GPU benar-benar bekerja — transfer memori H2D/D2H, CUDA context, dan alasan GPU menjadi pilihan mahal untuk request tunggal — topik chapter berikutnya.