Kernel GPU Umum

Julia memiliki perpustakaan bernama CUDAnative, yang meretas kompiler untuk menjalankan kode Anda di GPU.

menggunakan CuArrays, CUDanatif xs, ys, zs = CuArray(rand( 1024))), CuArray(rand(1024)),  CuArray(nol(1024))  fungsi kernel_vadd(keluar, A,  B) Saya = (blockIdx() .x-1) * blockDim(). x + threadIdx().x keluar = A + B  kembali akhir @cuda (1, panjang(xs )))  kernel_vadd(zs, xs, ys) @ tegaskan zs == xs + ys 

Apakah ini lebih baik daripada menulis CUDA C? Pada awalnya, mudah untuk salah mengira ini sebagai kenyamanan sintaksis sederhana, tetapi saya yakin bahwa ini membawa sesuatu yang baru secara fundamental ke meja. Abstraksi array Julia yang kuat ternyata sangat cocok untuk pemrograman GPU, dan harus menarik bagi peretas GPGPU terlepas dari apakah mereka sudah menggunakan bahasa tersebut. Dimensi Baru Bagi para ahli numerik, salah satu fitur pembunuh Julia adalah dukungan array N-dimensinya yang kuat. Ini meluas tidak hanya ke operasi “vektorisasi” tingkat tinggi seperti aritmatika penyiaran, tetapi juga ke loop dalam di kernel level terendah. Misalnya, ambil kernel CPU yang menambahkan dua larik 2D:

[:, :, 1, 1, 1]fungsi Menambahkan!(keluar, A , B) untuk Saya = 1: ukuran(A, 1) untuk J = 1: ukuran(A, 2) keluar[i,j] = A[i,j] + b[i,j] akhir akhir akhir

Kernel ini cepat, tetapi sulit untuk digeneralisasikan di berbagai jumlah dimensi. Perubahan yang diperlukan untuk mendukung array 3D, misalnya, kecil dan mekanis (tambahkan loop dalam ekstra), tetapi kami tidak dapat menulisnya menggunakan fungsi normal. Pembuatan kode Julia memungkinkan solusi elegan, jika sedikit misterius:

menggunakan Basis.Cartesian @generated fungsi Menambahkan!(keluar, A, B) N = ndims(keluar) mengutip @nloops $N Saya keluar mulai @nref($N, keluar, Saya) =   @nref($N, A, Saya) + @nref($N, B, Saya)  akhir akhir akhir 

NS @generated memungkinkan kita untuk menghubungkan ke spesialisasi kode Julia; ketika fungsi menerima matriks sebagai input, pembuatan kode kustom kami akan membuat dan menjalankan loop bersarang dua kali. Ini akan berperilaku sama dengan

 kami tambahkan!

fungsi di atas, tetapi untuk array dimensi apa pun. Jika Anda menghapus @generated Anda dapat melihat internal.

julia> menggunakan MakroTools julia> Menambahkan!(zs, xs, ys) |> perluas makro |> MakroTools.cantik kutipan untuk i_2 = indeks (keluar, 2) Tidak ada apa-apa untuk i_1 = indeks (keluar, 1) Tidak ada apa-apa keluar[i_1, i_2] = A[i_1, i_2] + b[i_1, i_2] Tidak ada apa-apa akhir Tidak ada apa-apa akhir akhir Jika Anda mencobanya dengan, katakanlah, input tujuh dimensi, Anda akan senang Anda tidak perlu menulis kode sendiri.

[:, :, 1, 1, 1] untuk i_7 = indeks (keluar, 7) untuk i_6 = indeks(keluar, 6) untuk i_5 = indeks

(keluar, 5) untuk i_4 = indeks( keluar, 4) untuk i_3 = indeks (keluar, 3 ) untuk i_2 = indeks (keluar, 2) untuk i_1 = indeks (keluar, 1) keluar[i_1, i_2, i_3, i_4, i_5, i_6, i_7] = A[i_1, i_2, i_3, i_4, i_5, i_6, i_7] + b[i_1, i_2, i_3, i_4, i_5, i_6, i_7] # Beberapa keluaran dihilangkan

Base.Cartesian adalah kerangka kerja yang kuat dan memiliki banyak alat yang lebih elegan, tetapi itu menggambarkan poin inti t.

Ini bonusnya. Penambahan jelas masuk akal atas sejumlah array input. Alat yang sama yang kami gunakan untuk dimensi generik dapat digunakan untuk menggeneralisasi jumlah input, juga:

@dihasilkan fungsi tambahan!(keluar, xs :: Vararg{Setiap,N}) di mana N  kutipan untuk Saya = 1 : panjang(keluar) keluar = @ncall $N (+) J -> xs[1]  akhir akhir akhir 

Sekali lagi, hapus @generated untuk melihat apa yang terjadi:

julia> tambahkan!(zs, xs, xs , ys, ys) |> perluas makro |> Alat Makro.mendandani mengutip untuk Saya = 1: panjang(keluar) keluar = (xs[1]) + (xs[2]) + (xs[4]) + (xs[4])  akhir akhir 

Jika kita menggabungkan ini, kita dapat membuat versi N-dimensi, argumen-N dari

kernel_vadd

pada GPU (di mana @cuindex menyembunyikan pengindeksan ND yang berantakan):

[:, :, 1, 1, 1]@dihasilkan fungsi kernel_vadd(keluar, xs:: NTupple{N}) di mana N mengutip SAYA = @cuindex(keluar) keluar[I] = @ncall $N (+) J -> xs[1][I] kembali akhir akhir @cuda (1, panjang( xs)) kernel_vadd(zs, (xs, ys ))

Kernel pendek ini sekarang dapat menambahkan sejumlah array dari dimensi apa pun; apakah itu masih hanya “CUDA dengan sintaks Julia”, atau itu sesuatu yang lebih?

Fungsi untuk Apa-apa

Julia memiliki lebih banyak trik. Ini secara otomatis mengkhususkan fungsi tingkat tinggi, yang berarti bahwa jika kita menulis:

fungsi kernel_zip2(F, keluar, A, B) Saya = (blockIdx().x-1) * blockDim().x + threadIdx().x keluar = F(A, B ) kembali akhir @cuda (1, panjang(xs)) kernel_zip2(+, zs, xs,  ys) 

Berperilaku dan melakukan persis suka kernel_vadd; tetapi kita dapat menggunakan fungsi biner apa pun tanpa kode tambahan. Sebagai contoh, sekarang kita dapat mengurangi dua array:

[:, :, 1, 1, 1]@cuda (1 , panjang(xs)) kernel_zip2(, zs, xs, ys)

Menggabungkan ini dengan yang di atas, kami memiliki semua alat yang kami butuhkan untuk menulis generik

broadcast

kernel (jika Anda tidak terbiasa dengan penyiaran array, anggap saja sebagai peta yang sedikit lebih umum ). Ini diimplementasikan dalam paket CuArrays yang dimuat sebelumnya, sehingga Anda dapat segera menulis:

[:, :, 1, 1, 1] julia> σ(x) = 1 / (1 + exp( x)) julia> σ.(xs) 1024elemen CuArray {Float64,1}: 0,547526 0,6911

(Yang, jika kita menggeneralisasi kernel_vadd dengan cara yang diuraikan a di atas, hanyalah sebuah “tambahan” menggunakan

σ

fungsi dan satu input.) Tidak ada petunjuk dalam kode kami , tetapi Julia akan mengompilasi kernel GPU khusus untuk menjalankan ekspresi tingkat tinggi ini. Julia juga akan menggabungkan beberapa siaran bersama-sama, jadi jika kita menulis ekspresi seperti Ini membuat panggilan kernel tunggal, tanpa alokasi memori atau array sementara yang diperlukan. Cukup keren – dan jauh dari jangkauan sistem lain yang saya tahu.

& Turunan Gratis

Jika Anda melihat yang asli kernel_vadd di atas, Anda akan melihat bahwa tidak ada jenis yang disebutkan. Julia adalah tipe bebek, bahkan pada GPU, dan kernel ini akan bekerja untuk apa pun yang mendukung operasi yang tepat.

Misalnya, input tidak memiliki menjadi CuArrays, asalkan terlihat seperti array dan dapat ditransfer ke GPU. Jika kita menambahkan rentang angka ke CuArray seperti ini:

@cuda (1, panjang(xs)) kernel_vadd(xs, xs, 1: 1024) 

Rentang 1:1024 tidak pernah benar-benar dialokasikan dalam memori; elemen-elemen [1, 2, ..., 1024] dihitung secara on-the-fly sesuai kebutuhan pada GPU. Tipe elemen array juga generik, dan hanya perlu mendukung +; jadi Int + Float64 berfungsi, seperti di atas, tapi kita juga bisa menggunakan tipe angka yang ditentukan pengguna. Contoh yang kuat adalah dual nomor. Bilangan ganda sebenarnya adalah sepasang bilangan, seperti bilangan kompleks; itu adalah nilai yang membawa turunannya sendiri.

[:, :, 1, 1, 1]julia> menggunakan ForwardDiff julia> F( x) = x^2 + 2x + 3 julia> x = ForwardDiff.Dua(5 , 1) Dua{Ruang kosong}(5,1 ) julia> F(x) Dua{Ruang kosong }(38,12)

Akhir

Dual

membawa nilai yang kami harapkan dari F (5^2 + 2*x + 3==38), tapi juga turunan (2x + 2==12) . Angka ganda memiliki rasio daya:kesederhanaan yang luar biasa tinggi dan sangat cepat , tetapi sama sekali tidak praktis di sebagian besar bahasa. Julia membuatnya sederhana, dan terlebih lagi, vektor angka ganda akan secara transparan melakukan perhitungan turunan pada GPU.

julia> xs = CuArray(ForwardDiff

.Dua.(1: 1024, 1) ) julia> F.(xs) 1024-elemen CuArray {ForwardDiff.Dua{Ruang kosong, Int64,1},1}: Ganda { Ruang kosong}(6,4) Dua{Ruang kosong}(11,6) Ganda {Ruang kosong}(18,8) julia> σ.(xs) 1024 -elemen CuArray{ForwardDiff.Dua{Ruang kosong, Float64,1},1}: Ganda { Ruang kosong}(0.731059,0.196612) Ganda {Ruang kosong}(0.880797 ,0,104994) Dua{Ruang kosong}(0,952574, 0.0451767)

Tidak hanya tidak ada overhead dibandingkan dengan tulisan tangan yang diperlukan kernel cuda untuk ini; tidak ada overhead sama sekali! Dalam tolok ukur saya, mengambil turunan menggunakan angka ganda adalah sama cepatnya dengan menghitung hanya nilainya dengan pelampung mentah. Cukup mengesankan. Dalam kerangka kerja pembelajaran mesin, biasanya membutuhkan "lapisan" untuk setiap kemungkinan fungsi aktivasi: sigmoid,

relu, tanh

dll. Memiliki trik ini di toolkit kami berarti backpropagation melalui setiap fungsi skalar akan bekerja secara gratis. Secara keseluruhan, kernel GPU di Julia adalah luar biasa generik, di seluruh jenis, dimensi dan arity. Ingin menyiarkan rentang bilangan bulat, matriks angka ganda, dan larik pelampung 6D? Silakan, dan satu kernel GPU yang sangat cepat akan memberi Anda hasilnya.

xs = CuArray(ForwardDiff.Dua.(randn (100,100), 1)) ys = CuArray(randn (1, 100, 5, 5, 5))  (1: 100) .* xs . / ys 100×100×5×5×5 Himpunan{ForwardDiff. Dua{Ruang kosong,Float64,1},5} :  [:, :, 1, 1, 1] =  Ganda {Ruang kosong}(0.0127874,-0.427122) 

Ganda{ Ruang kosong}(- 0.908558,-0.891798) Dua{Ruang kosong}(0.97554,-2,56273 ) Ganda {Ruang kosong}(-8.22101,-5.35079) Dua{Ruang kosong}(-7.13571, -4.27122) Dua{Ruang kosong}(2.14025,-8.91798)

Mesin penyiaran lengkap di CuArrays adalah 60 baris . Meskipun tidak sepenuhnya sepele, ini adalah jumlah fungsionalitas yang luar biasa untuk didapatkan dari kode sebanyak ini. CuArrays sendiri berada di bawah 400 baris sumber, sambil menyediakan hampir semua operasi larik umum (pengindeksan, penggabungan, permutedim, dll.) dengan cara umum yang serupa.

Kemampuan Julia untuk mengeluarkan kode khusus belum pernah terjadi sebelumnya, dan saya senang melihat ke mana arahnya di masa depan. Misalnya, akan relatif mudah untuk membangun kerangka kerja mirip Theano di Julia, dan membuat kernel khusus untuk komputasi yang lebih besar. Either way, saya pikir kita akan mendengar lebih banyak tentang Julia dan GPU seiring berjalannya waktu.

Kredit penuh untuk pekerjaan di balik ini kepada Tim Besard dan Jarrett Revels, masing-masing penulis yang luar biasa CUDAnative dan ForwardDiff.

Baca selengkapnya