Topik ini membahas cara memindahkan model PyTorch keluar dari ekosistem Python melalui ekspor ONNX. Pembahasan mencakup posisi ONNX sebagai format pertukaran model, proses ekspor dasar dengan torch.onnx.export, verifikasi hasil ekspor menggunakan ONNX Runtime, dan penyiapan batch dinamis agar satu file menerima banyak ukuran masukan. Setelah selesai, model tersimpan sebagai satu file .onnx yang dapat dijalankan tanpa PyTorch, beserta bukti bahwa keluarannya sama dengan versi PyTorch.
ONNX sebagai format pertukaran#
Selama berada di dalam Python, model PyTorch berwujud sebagai objek yang terikat pada interpreter, pustaka, dan kode kelasnya. Kondisi ini nyaman saat eksperimen, tetapi menyulitkan saat model perlu dijalankan di tempat lain, misalnya di program C++, aplikasi seluler, atau layanan yang tidak mengizinkan Python. Menyalin kode kelas dan file bobot saja tidak cukup karena bentuk model masih dijelaskan oleh kode Python, bukan oleh sebuah file mandiri.
ONNX (Open Neural Network Exchange) menyelesaikan masalah tersebut. ONNX adalah format terbuka yang menyimpan graf komputasi beserta bobotnya ke dalam satu file, umumnya berekstensi .onnx. Isi file itu bukan kode Python, melainkan daftar operator, yaitu operasi komputasi standar yang dikenal ONNX, beserta bentuk tensor yang diharapkan, sehingga file tersebut dapat dimuat oleh runtime yang mendukung format ini. Runtime yang paling umum digunakan adalah ONNX Runtime, yaitu mesin eksekusi buatan Microsoft yang tersedia untuk Python, C++, dan beberapa platform lainnya.
Dengan pemisahan ini, pengembangan dan pengerahan menjadi dua pekerjaan dengan alat yang berbeda. Pelatihan tetap berlangsung di PyTorch, sedangkan file .onnx menjadi batas antara keduanya: apa pun yang berada di sisi penerima tidak lagi membutuhkan PyTorch. Pembahasan di bawah menunjukkan sisi ekspornya terlebih dahulu, kemudian sisi pemuatannya.
Ekspor dasar#
Fungsi torch.onnx.export menerima model, satu contoh masukan, dan nama file tujuan. Contoh masukan berperan sebagai bahan perekaman: nilai sebenarnya tidak disimpan, tetapi bentuk dan alur operasinya diikuti untuk menyusun graf. Sebelum diekspor, model dipindahkan ke mode evaluasi dengan eval() agar layer seperti dropout dan batch normalization berperilaku seperti saat inferensi. Parameter input_names dan output_names memberi nama pada tensor masukan dan keluaran, yang kelak digunakan untuk merujuk keduanya pada saat dimuat.
import os
import torch
import torch.nn as nn
class JaringanKecil(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(32, 64)
self.fc2 = nn.Linear(64, 10)
def forward(self, x):
return self.fc2(torch.relu(self.fc1(x)))
torch.manual_seed(42)
model = JaringanKecil()
model.eval()
contoh = (torch.randn(4, 32),)
torch.onnx.export(
model,
contoh,
"model.onnx",
input_names=["masukan"],
output_names=["keluaran"],
)
print("Berkas tercipta :", os.path.exists("model.onnx"))
print("Ukuran berkas :", round(os.path.getsize("model.onnx") / 1024), "kB (kira-kira)")[torch.onnx] Obtain model graph for `JaringanKecil([...]` with `torch.export.export(..., strict=False)`...
[torch.onnx] Obtain model graph for `JaringanKecil([...]` with `torch.export.export(..., strict=False)`... ✅
[torch.onnx] Run decompositions...
[torch.onnx] Run decompositions... ✅
[torch.onnx] Translate the graph into ONNX...
[torch.onnx] Translate the graph into ONNX... ✅
[torch.onnx] Optimize the ONNX graph...
[torch.onnx] Optimize the ONNX graph... ✅
Berkas tercipta : True
Ukuran berkas : 4 kB (kira-kira)Baris-baris berawalan [torch.onnx] adalah laporan kemajuan dari pengekspor, bukan error. Sejak PyTorch 2.x, ekspor berjalan melalui torch.export untuk merekam graf, lalu mengurainya menjadi operator standar ONNX sebelum ditulis ke file. Keluaran menunjukkan file model.onnx berhasil dibuat dengan ukuran beberapa kilobita, wajar untuk model kecil dengan dua layer Linear.
Verifikasi dengan ONNX Runtime#
File hasil ekspor belum layak dikirim sebelum keluarannya dibandingkan dengan model aslinya. Bila keluaran keduanya sama pada toleransi yang dinyatakan, file .onnx benar-benar mewakili model PyTorch. Perbandingan ini menjadi uji terima baku sebelum file dipindahkan ke lingkungan lain.
Skrip berikut memuat model.onnx dengan ONNX Runtime, memberi masukan yang sama pada kedua bentuk, lalu membandingkan hasilnya:
import numpy as np
import onnxruntime as ort
import torch
import torch.nn as nn
class JaringanKecil(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(32, 64)
self.fc2 = nn.Linear(64, 10)
def forward(self, x):
return self.fc2(torch.relu(self.fc1(x)))
torch.manual_seed(42)
model = JaringanKecil().eval()
x = torch.randn(4, 32)
dengan_torch = model(x).detach().numpy()
sesi = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
print("Provider aktif:", sesi.get_providers())
dengan_onnx = sesi.run(["keluaran"], {"masukan": x.numpy()})[0]
print("Bentuk keluaran onnxruntime:", dengan_onnx.shape)
print("Keluaran sama (atol 1e-5):", np.allclose(dengan_torch, dengan_onnx, atol=1e-5))
print("Selisih maksimum :", float(np.abs(dengan_torch - dengan_onnx).max()))Provider aktif: ['CPUExecutionProvider']
Bentuk keluaran onnxruntime: (4, 10)
Keluaran sama (atol 1e-5): True
Selisih maksimum : 0.0Argumen providers=["CPUExecutionProvider"] memilih mesin eksekusi CPU secara eksplisit, sehingga skrip tidak bergantung pada deteksi otomatis. Keluaran menunjukkan hasil ONNX Runtime identik dengan keluaran PyTorch pada toleransi 1e-5. Perlu dicatat bahwa PyTorch pada skrip ini hanya berperan sebagai pembanding. Pihak yang menerima file .onnx cukup memasang onnxruntime, tanpa PyTorch sama sekali.
Batch dinamis dengan dynamic_shapes#
Ekspor dasar mengikat ukuran batch pada contoh masukan. File model.onnx di atas hanya menerima batch berukuran 4, dan ONNX Runtime akan menolak masukan lain karena bentuknya berbeda dari yang tercatat. Untuk layanan yang menerima jumlah data yang berubah-ubah, dimensi batch perlu dinyatakan dinamis saat ekspor.
Cara modernnya adalah menggunakan parameter dynamic_shapes, yang mengaitkan dimensi masukan dengan simbol torch.export.Dim:
import torch
import torch.nn as nn
import onnxruntime as ort
class JaringanKecil(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(32, 64)
self.fc2 = nn.Linear(64, 10)
def forward(self, x):
return self.fc2(torch.relu(self.fc1(x)))
torch.manual_seed(42)
model = JaringanKecil().eval()
batch = torch.export.Dim("batch")
torch.onnx.export(
model,
(torch.randn(4, 32),),
"model-dinamis.onnx",
input_names=["masukan"],
output_names=["keluaran"],
dynamic_shapes={"x": {0: batch}},
)
sesi = ort.InferenceSession("model-dinamis.onnx", providers=["CPUExecutionProvider"])
for n in (4, 16):
x = torch.randn(n, 32)
y = sesi.run(["keluaran"], {"masukan": x.numpy()})[0]
print(f"Batch {n:2d} -> bentuk keluaran {y.shape}")[torch.onnx] Obtain model graph for `JaringanKecil([...]` with `torch.export.export(..., strict=False)`...
[torch.onnx] Obtain model graph for `JaringanKecil([...]` with `torch.export.export(..., strict=False)`... ✅
[torch.onnx] Run decompositions...
[torch.onnx] Run decompositions... ✅
[torch.onnx] Translate the graph into ONNX...
[torch.onnx] Translate the graph into ONNX... ✅
[torch.onnx] Optimize the ONNX graph...
[torch.onnx] Optimize the ONNX graph... ✅
Batch 4 -> bentuk keluaran (4, 10)
Batch 16 -> bentuk keluaran (16, 10)Kunci "x" pada dynamic_shapes merujuk nama parameter pada forward, dan {0: batch} menyatakan dimensi ke-0, yaitu dimensi batch, sebagai simbol bernama batch. Keluaran membuktikan file tersebut kini menerima batch 4 maupun 16 tanpa diekspor ulang. Parameter lama bernama dynamic_axes masih diterima dan bekerja dengan cara serupa, tetapi pada jalur ekspor berbasis dynamo pesan peringatan menyarankan penggunaan dynamic_shapes.
Kesalahan umum#
Kesalahan pertama adalah mengekspor tanpa memanggil model.eval(). Bobot yang tersimpan tetap benar, tetapi perilaku layer seperti dropout ikut tercatat dalam mode pelatihan, sehingga keluaran file .onnx berubah-ubah saat digunakan. Perbaikannya adalah selalu memanggil model.eval() sebelum torch.onnx.export, lalu menjalankan verifikasi perbandingan sebagaimana pada bagian sebelumnya.
Kesalahan kedua adalah memberi masukan dengan bentuk berbeda dari contoh ekspor tanpa menyiapkan batch dinamis. ONNX Runtime menolak masukan itu dengan pesan yang menyebut ketidaksesuaian bentuk, padahal model aslinya sanggup menerima bentuk tersebut. Perbaikannya adalah mengekspor ulang dengan dynamic_shapes, atau mengekspor dengan contoh masukan yang bentuknya sama dengan bentuk yang akan digunakan di production.
Ringkasan#
- ONNX menyimpan graf komputasi dan bobot model ke dalam satu file
.onnxyang dapat dimuat oleh runtime lain, sehingga pihak penerima tidak membutuhkan PyTorch. torch.onnx.exportmerekam model melalui contoh masukan, daninput_namessertaoutput_namesmenamai tensor untuk dirujuk saat dimuat.- Verifikasi wajib dilakukan dengan ONNX Runtime menggunakan
providers=["CPUExecutionProvider"], yaitu membandingkan keluaran kedua bentuk pada toleransi yang dinyatakan. - Parameter
dynamic_shapesdengan simboltorch.export.Dimmembuat dimensi batch menjadi dinamis, sedangkandynamic_axesadalah API lamanya.
Referensi#
- torch.onnx — PyTorch documentation
— Dokumentasi ekspor ONNX, termasuk parameter
input_names,output_names, dandynamic_shapes. - Introduction to ONNX — PyTorch Tutorials — Pengenalan format ONNX dan cara menjalankan hasil ekspor dengan ONNX Runtime.
- ONNX documentation — ONNX — Spesifikasi format pertukaran model beserta daftar operator standarnya.
- ONNX Runtime Python API — ONNX Runtime — Cara memuat dan menjalankan file ONNX dengan Python.