Mentransfer array / kelas / catatan antar lokal
Dalam simulasi N-Body tipikal, di akhir setiap epoch, setiap lokasi perlu berbagi bagian dunianya sendiri (yaitu semua benda) ke lokasi lainnya. Saya mengerjakan ini dengan pendekatan pandangan-lokal (yaitu menggunakan on Loc
pernyataan). Saya mengalami beberapa perilaku aneh yang tidak dapat saya pahami, jadi saya memutuskan untuk membuat program pengujian, di mana hal-hal menjadi lebih rumit. Berikut kode untuk mereplikasi eksperimen.
proc log(args...?n) {
writeln("[locale = ", here.id, "] [", datetime.now(), "] => ", args);
}
const max: int = 50000;
record stuff {
var x1: int;
var x2: int;
proc init() {
this.x1 = here.id;
this.x2 = here.id;
}
}
class ctuff {
var x1: int;
var x2: int;
proc init() {
this.x1 = here.id;
this.x2 = here.id;
}
}
class wrapper {
// The point is that total size (in bytes) of data in `r`, `c` and `a` are the same here, because the record and the class hold two ints per index.
var r: [{1..max / 2}] stuff;
var c: [{1..max / 2}] owned ctuff?;
var a: [{1..max}] int;
proc init() {
this.a = here.id;
}
}
proc test() {
var wrappers: [LocaleSpace] owned wrapper?;
coforall loc in LocaleSpace {
on Locales[loc] {
wrappers[loc] = new owned wrapper();
}
}
// rest of the experiment further down.
}
Dua perilaku menarik terjadi di sini.
1. Memindahkan data
Sekarang, setiap instance wrapper
dalam array wrappers
harus tinggal di lokalnya. Secara khusus, referensi ( wrappers
) akan tinggal di lokal 0, namun data internal ( r
, c
, a
) harus hidup di lokal masing-masing. Jadi kami mencoba untuk memindahkan beberapa dari lokal 1 ke lokal 3, seperti:
on Locales[3] {
var timer: Timer;
timer.start();
var local_stuff = wrappers[1]!.r;
timer.stop();
log("get r from 1", timer.elapsed());
log(local_stuff);
}
on Locales[3] {
var timer: Timer;
timer.start();
var local_c = wrappers[1]!.c;
timer.stop();
log("get c from 1", timer.elapsed());
}
on Locales[3] {
var timer: Timer;
timer.start();
var local_a = wrappers[1]!.a;
timer.stop();
log("get a from 1", timer.elapsed());
}
Anehnya, pengaturan waktu saya menunjukkan itu
Terlepas dari size (
const max
), waktu pengiriman array dan record tetap konstan, yang tidak masuk akal bagi saya. Aku bahkan memeriksanyachplvis
, dan ukurannyaGET
benar - benar bertambah, tetapi waktunya tetap sama.Waktu untuk mengirim bidang kelas meningkat seiring waktu, yang masuk akal, tetapi cukup lambat dan saya tidak tahu kasus mana yang harus dipercaya di sini.
2. Mengkueri lokal secara langsung.
Untuk mengungkap masalah ini, saya juga menanyakan .locale.id
beberapa variabel secara langsung. Pertama, kami mengkueri data, yang kami harapkan ada di lokal 2, dari lokal 2:
on Locales[2] {
var wrappers_ref = wrappers[2]!; // This is always 1 GET from 0, okay.
log("array",
wrappers_ref.a.locale.id,
wrappers_ref.a[1].locale.id
);
log("record",
wrappers_ref.r.locale.id,
wrappers_ref.r[1].locale.id,
wrappers_ref.r[1].x1.locale.id,
);
log("class",
wrappers_ref.c.locale.id,
wrappers_ref.c[1]!.locale.id,
wrappers_ref.c[1]!.x1.locale.id
);
}
Dan hasilnya adalah:
[locale = 2] [2020-12-26T19:36:26.834472] => (array, 2, 2)
[locale = 2] [2020-12-26T19:36:26.894779] => (record, 2, 2, 2)
[locale = 2] [2020-12-26T19:36:27.023112] => (class, 2, 2, 2)
Yang diharapkan. Namun, jika kita menanyakan lokasi dari data yang sama pada lokal 1, maka kita mendapatkan:
[locale = 1] [2020-12-26T19:34:28.509624] => (array, 2, 2)
[locale = 1] [2020-12-26T19:34:28.574125] => (record, 2, 2, 1)
[locale = 1] [2020-12-26T19:34:28.700481] => (class, 2, 2, 2)
Menyiratkan bahwa wrappers_ref.r[1].x1.locale.id
tinggal di lokal 1, meskipun itu jelas harus di lokal 2 . Dugaan saya satu-satunya adalah bahwa pada saat .locale.id
dijalankan, data (yaitu .x
record) sudah dipindahkan ke query lokal (1).
Jadi secara keseluruhan, bagian kedua dari eksperimen mengarah ke pertanyaan sekunder, sementara tidak menjawab bagian pertama.
CATATAN: semua percobaan dijalankan dengan -nl 4
dalam chapel/chapel-gasnet
gambar buruh pelabuhan.
Jawaban
Pengamatan yang bagus, biarkan saya melihat apakah saya bisa menjelaskan.
Sebagai catatan awal, pengaturan waktu apa pun yang diambil dengan gambar Docker gasnet harus diambil dengan butiran garam karena gambar tersebut mensimulasikan eksekusi di beberapa node menggunakan sistem lokal Anda daripada menjalankan setiap lokal pada node komputasi sendiri seperti yang dimaksudkan di Chapel. Hasilnya, ini berguna untuk mengembangkan program memori terdistribusi, tetapi karakteristik kinerja cenderung sangat berbeda daripada berjalan pada cluster atau superkomputer yang sebenarnya. Meskipun demikian, ini masih dapat berguna untuk mendapatkan pengaturan waktu yang kasar (misalnya, pengamatan "ini membutuhkan waktu lebih lama") atau untuk menghitung komunikasi menggunakan chplvis
atau modul CommDiagnostics .
Sehubungan dengan pengamatan Anda tentang pengaturan waktu, saya juga mengamati bahwa kasus array-of-class jauh lebih lambat, dan saya yakin saya dapat menjelaskan beberapa perilaku:
Pertama, penting untuk dipahami bahwa setiap komunikasi lintas node dapat dikarakterisasi menggunakan rumus seperti alpha + beta*length
. Anggaplah alpha
mewakili biaya dasar untuk melakukan komunikasi, terlepas dari panjangnya. Ini mewakili biaya panggilan ke bawah melalui tumpukan perangkat lunak untuk sampai ke jaringan, meletakkan data di kabel, menerimanya di sisi lain, dan mendapatkannya kembali melalui tumpukan perangkat lunak ke aplikasi di sana. Nilai alfa yang tepat akan bergantung pada faktor-faktor seperti jenis komunikasi, pilihan tumpukan perangkat lunak, dan perangkat keras fisik. Sementara itu, anggaplah beta
sebagai mewakili biaya per-byte dari komunikasi di mana, saat Anda intuisi, pesan yang lebih panjang tentu lebih mahal karena ada lebih banyak data untuk diletakkan di kabel, atau berpotensi untuk buffer atau disalin, tergantung pada bagaimana komunikasi diimplementasikan.
Dalam pengalaman saya, nilai alpha
biasanya mendominasi beta
untuk sebagian besar konfigurasi sistem. Itu tidak berarti bahwa melakukan transfer data yang lebih lama itu gratis, tetapi varians dalam waktu eksekusi cenderung jauh lebih kecil untuk transfer yang lebih lama vs. lebih pendek daripada untuk melakukan satu transfer versus banyak. Akibatnya, saat memilih antara melakukan satu transfer n
elemen vs. n
transfer 1 elemen, Anda hampir selalu menginginkan yang pertama.
Untuk menyelidiki pengaturan waktu Anda, saya mengurung bagian kode waktunya dengan panggilan ke CommDiagnostics
modul sebagai berikut:
resetCommDiagnostics();
startCommDiagnostics();
...code to time here...
stopCommDiagnostics();
printCommDiagnosticsTable();
dan menemukan, seperti yang Anda lakukan dengan chplvis
, bahwa jumlah komunikasi yang diperlukan untuk melokalkan array record atau array int adalah konstan seperti yang saya variasikan max
, misalnya:
lokal | Dapatkan | eksekusi_on |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
2 | 0 | 0 |
3 | 21 | 1 |
Ini konsisten dengan apa yang saya harapkan dari implementasi: Bahwa untuk array tipe nilai, kami melakukan sejumlah komunikasi tetap untuk mengakses meta-data array, dan kemudian mengkomunikasikan elemen array itu sendiri dalam satu transfer data untuk mengamortisasi biaya overhead (hindari membayar banyak alpha
biaya).
Sebaliknya, saya menemukan bahwa jumlah komunikasi untuk melokalkan array kelas sebanding dengan ukuran array. Misalnya, untuk nilai default 50.000 max
, saya melihat:
lokal | Dapatkan | taruh | eksekusi_on |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 |
2 | 0 | 0 | 0 |
3 | 25040 | 25000 | 1 |
Saya percaya alasan untuk perbedaan ini berkaitan dengan fakta bahwa c
adalah array owned
kelas, di mana hanya satu variabel kelas yang dapat "memiliki" ctuff
objek tertentu pada satu waktu. Akibatnya, saat menyalin elemen larik c
dari satu tempat ke tempat lain, Anda tidak hanya menyalin data mentah, seperti kasus record dan integer, tetapi juga melakukan transfer kepemilikan per elemen. Ini pada dasarnya memerlukan pengaturan nilai jarak jauh nil
setelah menyalin nilainya ke variabel kelas lokal. Dalam implementasi kami saat ini, ini tampaknya dilakukan dengan menggunakan remote get
untuk menyalin nilai kelas jarak jauh ke lokal, diikuti oleh remote put
untuk mengatur nilai jarak jauh nil
, oleh karena itu, kami memiliki elemen get dan put per array, menghasilkan O (n) komunikasi daripada O (1) seperti dalam kasus sebelumnya. Dengan upaya tambahan, kami berpotensi memiliki kompiler yang mengoptimalkan kasus ini, meskipun saya yakin akan selalu lebih mahal daripada yang lain karena kebutuhan untuk melakukan transfer kepemilikan.
Saya menguji hipotesis bahwa owned
kelas yang mengakibatkan overhead tambahan dengan mengubah Anda ctuff
objek dari yang owned
untuk unmanaged
, yang menghilangkan setiap semantik kepemilikan dari pelaksanaan. Ketika saya melakukan ini, saya melihat sejumlah komunikasi yang konstan, seperti dalam kasus nilai:
lokal | Dapatkan | eksekusi_on |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
2 | 0 | 0 |
3 | 21 | 1 |
Saya percaya ini mewakili fakta bahwa sekali bahasa tidak perlu mengelola kepemilikan variabel kelas, itu dapat dengan mudah mentransfer nilai penunjuk mereka dalam satu transfer lagi.
Di luar catatan kinerja ini, penting untuk memahami perbedaan semantik utama antara kelas dan rekaman saat memilih mana yang akan digunakan. Objek kelas dialokasikan di heap, dan variabel kelas pada dasarnya adalah referensi atau penunjuk ke objek tersebut. Jadi, ketika variabel kelas disalin dari satu tempat ke tempat lain, hanya penunjuk yang disalin, dan objek asli tetap di tempatnya (baik atau buruk). Sebaliknya, variabel record mewakili objek itu sendiri, dan dapat dianggap dialokasikan "di tempat" (misalnya, di stack untuk variabel lokal). Ketika variabel record disalin dari satu tempat ke tempat lain, objek itu sendiri (yaitu, nilai bidang record) yang disalin, menghasilkan salinan baru dari objek itu sendiri. Lihat pertanyaan SO ini untuk lebih jelasnya.
Pindah ke pengamatan kedua Anda, saya percaya bahwa interpretasi Anda benar, dan ini mungkin bug dalam penerapannya (saya perlu memperbaikinya sedikit lagi agar percaya diri). Secara khusus, saya pikir Anda benar bahwa apa yang terjadi adalah yang wrappers_ref.r[1].x1
sedang dievaluasi, dengan hasil disimpan dalam variabel lokal, dan bahwa .locale.id
kueri diterapkan ke variabel lokal yang menyimpan hasil daripada bidang asli. Saya menguji teori ini dengan membawa a ref
ke lapangan dan kemudian mencetak locale.id
ref itu, sebagai berikut:
ref x1loc = wrappers_ref.r[1].x1;
...wrappers_ref.c[1]!.x1.locale.id...
dan itu sepertinya memberikan hasil yang benar. Saya juga melihat kode yang dihasilkan yang sepertinya menunjukkan bahwa teori kami benar. Saya tidak percaya bahwa penerapannya harus seperti ini, tetapi perlu memikirkannya lebih dalam sebelum menjadi yakin. Jika Anda ingin membuka bug terhadap hal ini di halaman terbitan Chapel GitHub , untuk diskusi lebih lanjut di sana, kami akan menghargainya.