Distributed dan GPU

Komputasi pada beberapa proses dengan Distributed beserta pengenalan CUDA.jl untuk GPU.

Daftar isi

Pemisahan proses memperluas paralelisme melampaui satu komputer, karena setiap proses Julia memiliki memori sendiri dan berkomunikasi melalui pesan. Pustaka standar Distributed menyediakan perangkatnya, mulai dari menambah proses pekerja, menjalankan kode di pekerja tertentu, hingga peta paralel pmap, sedangkan CUDA.jl membawa bentuk paralelisme yang sama ke kartu grafis. Topik ini membahas pengoperasian proses pekerja, pertukaran data antar proses, dan pengenalan model pemrograman GPU beserta batas lingkungan praktiknya. Setelah topik ini dipraktikkan, batasan inti satu mesin tidak lagi membatasi desain program.

Menambah proses pekerja#

Proses pekerja (worker) adalah proses Julia tambahan yang dikelola dari proses utama. Fungsi addprocs(n) menambahkan n pekerja pada mesin yang sama, dan setiap pekerja dikenali melalui nomor identitasnya, sedangkan proses utama selalu bernomor satu. Pekerja baru tidak membawa definisi dari proses utama, sehingga kode yang dipakai pekerja dikirim terlebih dahulu dengan makro @everywhere.

julia Contoh: menambah dua pekerja dan memanggil kode di sana
using Distributed

pekerja = addprocs(2)
@everywhere kuadrat(x) = x * x
println(remotecall_fetch(kuadrat, pekerja[1], 7))
println(remotecall_fetch(kuadrat, pekerja[2], 9))
rmprocs(pekerja...)
text Keluaran
49
81

Contoh di atas memperlihatkan siklus lengkap pemakaian pekerja. remotecall_fetch mengirim fungsi beserta argumennya ke pekerja yang ditunjuk, menjalankan fungsi tersebut di sana, dan mengembalikan hasilnya kepada pemanggil dalam satu langkah tunggu. rmprocs mengakhiri pekerja yang tidak lagi dibutuhkan, dan pemanggilan ini juga sebaiknya dilakukan pada akhir program agar seluruh proses ditutup dengan rapi. Argumen yang dikirim disalin ke memori pekerja, karena kedua proses tidak berbagi memori, dan sifat inilah yang membedakan model proses dengan model thread.

Menyebarkan definisi dengan @everywhere#

Makro @everywhere ekspresi mengeksekusi ekspresi pada proses utama dan seluruh pekerja sekaligus. Definisi fungsi, konstanta, dan pemuatan pustaka yang diperlukan pekerja dikirim melalui makro ini. Apabila pekerja ditambahkan setelah definisi disebarkan, pekerja baru tetap kosong, sehingga urutan kerja yang lazim adalah menambah pekerja terlebih dahulu, baru menyebarkan definisi.

julia Contoh: definisi tersebar dan pemetaan paralel
using Distributed

pekerja = addprocs(2)
@everywhere mulai_saya(x) = x + 100
println(pmap(mulai_saya, 1:6))
rmprocs(pekerja...)
text Keluaran
[101, 102, 103, 104, 105, 106]

Fungsi pmap memetakan fungsi ke koleksi dan mendistribusikan tiap pemanggilan ke pekerja yang bebas, lalu mengumpulkan hasil dalam urutan masukan, sebagaimana keluaran contoh di atas. Pembagian tugasnya dinamis, sehingga pekerja yang lebih cepat menerima bagian lebih banyak, dan mekanisme ini membuat pmap cocok untuk pekerjaan yang biayanya tidak seragam. Keluaran yang terurut pasti pada contoh menunjukkan bahwa paralelisme pada model proses tidak mengubah isi hasil, melainkan hanya kecepatan mengerjakannya.

Peta paralel dengan fungsi besar#

Untuk pekerjaan yang argumennya besar, mengirim fungsi dan data per pemanggilan menimbulkan biaya salin yang terasa. Pola yang menguranginya adalah menyebarkan data sekali ke setiap pekerja melalui @everywhere, lalu mengirim hanya indeks atau parameter kecil melalui remotecall_fetch. Pola ini juga berlaku untuk pemuatan berkas: pekerja memuat berkas kode bersama melalui @everywhere include("nama.jl"), dan seluruh definisi berkas itu tersedia di setiap proses.

julia Contoh: data tersebar sekali, parameter dikirim per pemanggilan
using Distributed

pekerja = addprocs(2)
@everywhere data_saya = collect(10.0:10.0:40.0)
@everywhere ambil(x) = data_saya[x] / 2
println(remotecall_fetch(ambil, pekerja[1], 2))
println(remotecall_fetch(ambil, pekerja[2], 4))
rmprocs(pekerja...)
text Keluaran
10.0
20.0

Pada contoh di atas, vektor data dibangun di memori setiap pekerja melalui @everywhere, sehingga pemanggilan berikutnya hanya mengirim satu indeks. Pola ini berlaku sama untuk data hasil pembacaan berkas besar, dan mengurangi lalu lintas pesan menjadi sekali kirim per pekerja. Untuk kluster yang melampaui satu mesin, addprocs menerima bentuk lain seperti daftar nama mesin dengan pengaturan SSH, dan perangkatnya sama persis, sehingga program yang berjalan dengan dua pekerja lokal juga berjalan pada kluster tanpa perubahan kode.

Pengenalan CUDA.jl#

CUDA.jl membawa model array Julia ke kartu grafis NVIDIA. Array yang tersimpan di memori GPU berbentuk CuArray, dan operasi pada array itu dikompilasi menjadi kernel yang dieksekusi ribuan thread GPU sekaligus. Bentuk kodenya mirip pemrograman Julia biasa, yaitu operasi per elemen dan broadcasting, sedangkan pemindahan data antara memori utama dan memori GPU dinyatakan eksplisit melalui CuArray dan Array.

julia
# tidak-dijalankan
# sandbox praktikum tidak memiliki kartu grafis NVIDIA,
# sehingga contoh GPU hanya disajikan sebagai bentuk kodenya.
using CUDA

x = CuArray([1.0, 2.0, 3.0, 4.0])
y = x .^ 2 + 1.0
println(Array(y))

Bentuk kode di atas menunjukkan pola yang sama dengan broadcasting pada modul Broadcasting, hanya tujuan eksekusinya berbeda. Pemanggilan CuArray menyalin data ke GPU, operasi .^ 2 .+ 1.0 dijadwalkan sebagai kernel di sana, dan Array menyalin hasilnya kembali. Sumber kecepatannya adalah banyaknya inti yang dieksekusi serentak, bukan perbedaan bahasa, dan biayanya adalah transfer data dua arah yang perlu diperhitungkan pada setiap tahap. Apabila pekerjaan berukuran kecil diulang berkali-kali, biaya transfernya dapat melampaui keuntungan GPU, sehingga GPU tepat untuk pekerjaan besar per pemanggilan.

Kesalahan umum#

Kesalahan pertama adalah memanggil fungsi yang belum dikirim ke pekerja. Pekerja baru tidak membawa definisi dari proses utama, sehingga remotecall_fetch pada fungsi yang belum disebarkan berakhir dengan UndefVarError di sisi pekerja. Perbaikannya adalah mengirim definisi melalui @everywhere sebelum pemanggilan pertama, sebagaimana urutan contoh pada topik ini.

Kesalahan kedua adalah memperlakukan data antar proses seolah berbagi memori. Mengubah array di proses utama tidak mengubah salinannya di pekerja, sehingga hasil yang dikembalikan pekerja perlu dikumpulkan secara eksplisit melalui nilai kembalian pemanggilan. Kesalahan ketiga adalah lupa menutup pekerja pada program yang menambahkan pekerja berkali-kali, karena setiap addprocs menambah proses sistem operasi yang tetap berjalan sampai diakhiri. Panggilan rmprocs pada akhir pekerjaan, atau pelaporan jumlah pekerja pada catatan program, menjaga sumber daya tetap bersih.

Ringkasan#

  • Distributed menambah proses pekerja dengan addprocs, menyebarkan definisi dengan @everywhere, dan memanggil kode pekerja melalui remotecall_fetch serta pmap.
  • Data antar proses disalin per pesan, sehingga data besar dikirim sekali per pekerja, sedangkan parameter kecil dikirim berulang.
  • CUDA.jl menerapkan pola array Julia pada GPU melalui CuArray dan broadcasting, dengan transfer data dua arah sebagai biaya yang diperhitungkan.

Referensi#