Kernel generik menuliskan inti komputasi vektor satu kali untuk seluruh dtype dan lebar, sedangkan compiler menyiapkan versi terkompilasi untuk setiap kombinasi yang dipanggil. Topik ini membahas deklarasi kernel berparameter dtype dan lebar, perpaduannya dengan perulangan comptime for, serta pemanggilan satu kernel untuk beberapa kombinasi tipe dalam satu program. Setelah topik ini dibaca, penjumlahan lane dan rata-rata kuadrat dapat ditangani satu body untuk tipe pecahan maupun bilangan bulat.
Satu body untuk banyak kombinasi#
Kernel pada konteks ini adalah fungsi yang menerima satu vektor SIMD dan menghasilkan nilai terhitungnya, misalnya jumlah seluruh lane. Deklarasinya memakai parameter dtype: DType dan lebar: Int pada kurung siku, sehingga tipe vektornya ditulis SIMD[dtype, lebar] dan nilai kembaliannya Scalar[dtype]. Nama Scalar menamai nilai SIMD berlebar satu secara generik, sehingga hasil kernel untuk float64 bertipe Scalar[DType.float64] dan untuk int32 bertipe Scalar[DType.int32]. Satu deklarasi semacam ini menggantikan duplikasi fungsi per pasangan dtype dan lebar.
Setiap pemanggilan dengan nilai parameter yang berbeda membangun satu spesialisasi baru dari body yang sama. Spesialisasi untuk kombinasi (DType.float64, 4) berisi kode yang menjumlahkan empat lane, sedangkan spesialisasi untuk (DType.float32, 8) berisi kode yang menjumlahkan delapan lane. Kode sumber tidak bertambah, tetapi setiap versi terkompilasi tetap menghitung dengan tipe dan lebar yang melekat padanya.
Kernel semacam ini menjadi bahan dasar program komputasi yang lebih besar. Pemrosesan citra, pengolahan suara, dan perhitungan statistik memproses datanya dalam potongan vektor, dan kernelnya ditulis sekali untuk melayani data pecahan maupun bilangan bulat. Pemanggil kernel cukup memilih kombinasi dtype dan lebar yang sesuai perangkat kerasnya, sedangkan body yang dirawat tetap satu.
Kernel dengan perulangan comptime#
Kernel berikut memadukan parameter kurung siku dengan perulangan compile-time. Fungsi jumlah_lanes menguraikan penjumlahan lane dengan comptime for yang rentangnya mengikuti parameter lebar, sedangkan kuadrat_rata menghitung rata-rata kuadrat lane dan mengonversi hasilnya ke Float64. Program memanggil jumlah_lanes untuk ketiga kombinasi dan kuadrat_rata untuk dua di antaranya.
def jumlah_lanes[dtype: DType, lebar: Int](v: SIMD[dtype, lebar]) -> Scalar[dtype]:
var total: Scalar[dtype] = Scalar[dtype](0)
comptime for i in range(lebar):
total += v[i]
return total
def kuadrat_rata[dtype: DType, lebar: Int](v: SIMD[dtype, lebar]) -> Float64:
return (v * v).cast[DType.float64]().reduce_add() / Float64(lebar)
def main():
var f = SIMD[DType.float64, 4](1.0, 2.0, 3.0, 4.0)
var g = SIMD[DType.float32, 8](1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0)
var n = SIMD[DType.int32, 2](5, 7)
print(jumlah_lanes[DType.float64, 4](f))
print(jumlah_lanes[DType.float32, 8](g))
print(jumlah_lanes[DType.int32, 2](n))
print(kuadrat_rata[DType.float64, 4](f))
print(kuadrat_rata[DType.int32, 2](n))# jalankan program kernel generik untuk tiga kombinasi tipe
mojo run kernel.mojo10.0
36.0
12
7.5
37.0Perulangan comptime for i in range(lebar) pada jumlah_lanes disalin sebanyak lebar kali untuk setiap spesialisasi, sehingga kernel untuk lebar 4 berisi empat penambahan dan kernel untuk lebar 8 berisi delapan. Deklarasi var total: Scalar[dtype] = Scalar[dtype](0) menetapkan tipe akumulator mengikuti dtype, dan keluaran 12 tanpa desimal berasal dari spesialisasi int32 yang menghitung 5 ditambah 7. Fungsi kuadrat_rata memanggil cast[DType.float64]() sebelum pembagian agar hasilnya pecahan, dan Float64(lebar) mengonversi parameter menjadi bilangan pecahan sebagai pembaginya. Keluaran 7.5 berasal dari (1.0 + 4.0 + 9.0 + 16.0) / 4, sedangkan 37.0 berasal dari (25.0 + 49.0) / 2 pada spesialisasi int32.
Metode cast pada kuadrat_rata juga menerima parameter compile-time, yaitu cast[DType.float64](), sehingga sintaks kurung siku yang sama berlaku pada metode maupun fungsi bebas. Konversi ke float64 dilakukan sebelum reduce_add() sehingga penjumlahan antarlane dan pembagiannya berjalan dalam tipe pecahan. Pembagian terakhir memakai Float64(lebar) karena parameter lebar bertipe Int.
Apabila perlakuan menurut tipe memang berbeda, pernyataan comptime if dapat ditambahkan pada body kernel yang sama untuk memilih cabang menurut dtype. Bentuk paling umumnya tetap satu body yang tidak menyeleksi tipe sama sekali, karena operasi aritmetika SIMD sudah didefinisikan untuk seluruh dtype. Kedua pola tersebut memakai parameter yang sama, sehingga penambahannya tidak mengubah bentuk pemanggilan.
Alasan satu body untuk semua tipe#
Tanpa kernel generik, setiap kombinasi dtype dan lebar menuntut salinan fungsi yang ditulis dan dirawat sendiri-sendiri. Tiga kombinasi pada program di atas berarti tiga fungsi dengan body yang mirip apabila ditulis manual, dan setiap perbaikan algoritma diulang di ketiganya. Kernel generik menyimpan logika di satu tempat, sedangkan compiler menurunkan spesialisasinya dari pemanggilan yang ada.
Spesialisasi yang dibangun compiler juga bebas biaya pemilihan tipe saat program berjalan, karena tipe sudah melekat pada versi yang dipanggil. Kesalahan tipe, misalnya pembagian tanpa konversi lebar, muncul pada waktu kompilasi untuk kombinasi yang dipanggil, bukan sebagai perilaku tak terduga saat program berjalan. Struktur ini membuat pengembangan kernel bergerak dari satu kombinasi yang benar menuju generalisasi parameternya.
Kesalahan umum#
- Menetapkan akumulator dengan tipe tetap. Deklarasi
var total = 0di dalam kernel generik menjadikantotalbertipeInt, dan penambahantotal += v[i]ditolak dengan pesanvalue passed to 'rhs' cannot be converted from 'Scalar[dtype]' to 'Int'. Perbaikannya adalahvar total: Scalar[dtype] = Scalar[dtype](0)yang mengikuti dtype parameternya. - Membagi hasil kernel dengan
lebartanpa konversi. Ekspresijumlah / lebarditolak dengan pesanvalue passed to 'rhs' cannot be converted from 'Int' to 'Float64'karenalebarbertipeInt. Perbaikannya adalah pembagiFloat64(lebar)sebagaimana dipakai pada fungsikuadrat_rata. - Menukar urutan parameter saat pemanggilan eksplisit. Pemanggilan
jumlah_lanes[2, DType.int32]ditolak dengan pesan'jumlah_lanes' parameter 'dtype' has 'DType' type, but value has type 'IntLiteral[2]'. Perbaikannya adalah mengikuti urutan deklarasi, yaitu dtype lebih dahulu, sehingga pemanggilannya menjadijumlah_lanes[DType.int32, 2].
Ringkasan#
- Kernel generik berparameter
dtypedanlebarmenuliskanSIMD[dtype, lebar]pada argumen danScalar[dtype]pada hasilnya. comptime fordenganrange(lebar)menyesuaikan jumlah salinan body dengan lebar, sedangkancomptime ifmenambah cabang menurut dtype apabila perlakuannya berbeda.- Setiap kombinasi yang dipanggil menjadi spesialisasi terpisah tanpa duplikasi sumber dan tanpa pemilihan tipe saat program berjalan.
- Konversi eksplisit, misalnya
Float64(lebar), wajib ketika parameter bertemu bilangan pecahan.
Referensi#
- Parameterized declarations — Mojo Manual — Dokumentasi resmi deklarasi berparameter yang menjadi dasar kernel generik.
- Mojo standard library — Modular — Dokumentasi tipe SIMD dan Scalar pada pustaka standar Mojo.