Praktik terbaik alokasi memori inisialisasi multicore / NUMA / inisialisasi

17

Ketika perhitungan terbatas bandwidth memori dilakukan dalam lingkungan memori bersama (mis. Berulir melalui OpenMP, Pthreads, atau TBB), ada dilema tentang bagaimana memastikan bahwa memori didistribusikan dengan benar di seluruh memori fisik , sehingga masing-masing thread kebanyakan mengakses memori pada sebuah bus memori "lokal". Meskipun antarmuka tidak portabel, sebagian besar sistem operasi memiliki cara untuk mengatur afinitas utas (mis. pthread_setaffinity_np()Pada banyak sistem POSIX, sched_setaffinity()di Linux, SetThreadAffinityMask()pada Windows). Ada juga perpustakaan seperti hwloc untuk menentukan hirarki memori, tetapi sayangnya, sebagian besar sistem operasi belum menyediakan cara untuk mengatur kebijakan memori NUMA. Linux adalah pengecualian penting, dengan libnumamemungkinkan aplikasi untuk memanipulasi kebijakan memori dan migrasi halaman pada granularity halaman (dalam arus utama sejak 2004, dengan demikian tersedia secara luas). Sistem operasi lain mengharapkan pengguna untuk mematuhi kebijakan "sentuhan pertama" implisit.

Bekerja dengan kebijakan "sentuhan pertama" berarti bahwa penelepon harus membuat dan mendistribusikan utas dengan afinitas apa pun yang mereka rencanakan untuk digunakan nanti ketika pertama kali menulis ke memori yang baru dialokasikan. (Sangat sedikit sistem yang dikonfigurasikan sehingga malloc()benar - benar menemukan halaman, itu hanya menjanjikan untuk menemukan mereka ketika mereka benar-benar rusak, mungkin oleh utas berbeda.) Ini menyiratkan bahwa alokasi menggunakan calloc()atau segera menginisialisasi memori setelah alokasi menggunakan memset()berbahaya karena akan cenderung untuk kesalahan semua memori ke bus memori inti menjalankan utas pengalokasian, yang mengarah ke bandwidth memori terburuk saat memori diakses dari banyak utas. Hal yang sama berlaku untuk newoperator C ++ yang bersikeras menginisialisasi banyak alokasi baru (misstd::complex). Beberapa pengamatan tentang lingkungan ini:

  • Alokasi dapat dibuat "thread threaded", tetapi sekarang alokasi menjadi dicampur ke dalam model threading yang tidak diinginkan untuk perpustakaan yang mungkin harus berinteraksi dengan klien menggunakan model threading yang berbeda (mungkin masing-masing dengan kumpulan thread mereka sendiri).
  • RAII dianggap sebagai bagian penting dari C ++ idiomatik, tetapi tampaknya berbahaya secara aktif untuk kinerja memori di lingkungan NUMA. Penempatan newdapat digunakan dengan memori yang dialokasikan melalui malloc()atau dari rutinitas libnuma, tetapi ini mengubah proses alokasi (yang saya percaya perlu).
  • EDIT: Pernyataan saya sebelumnya tentang operator newtidak benar, itu dapat mendukung beberapa argumen, lihat balasan Chetan. Saya percaya masih ada kekhawatiran mendapatkan perpustakaan atau wadah STL untuk menggunakan afinitas yang ditentukan. Beberapa bidang dapat dikemas dan mungkin tidak nyaman untuk memastikan bahwa, misalnya, std::vectorrealokasi dengan manajer konteks yang benar aktif.
  • Setiap utas dapat mengalokasikan dan kesalahan memori pribadinya sendiri, tetapi kemudian mengindeks ke daerah tetangga lebih rumit. (Pertimbangkan produk matriks-vektor yang jarang Anda dengan partisi baris dari matriks dan vektor; pengindeksan bagian yang tidak dimiliki memerlukan struktur data yang lebih rumit ketika tidak bersebelahan dalam memori virtual.)ySEBUAHxxx

Apakah ada solusi untuk alokasi / inisialisasi NUMA yang dianggap idiomatis? Sudahkah saya meninggalkan gotcha penting lainnya?

(Saya tidak bermaksud untuk C saya ++ contoh menyiratkan penekanan pada bahasa yang, namun C ++ bahasa mengkodekan beberapa keputusan tentang manajemen memori yang bahasa seperti C tidak, sehingga ada cenderung lebih tahan ketika menunjukkan bahwa C ++ programmer melakukan hal- hal-hal berbeda.)

Jed Brown
sumber

Jawaban:

7

Salah satu solusi untuk masalah ini yang cenderung saya sukai adalah memisahkan benang dan tugas (MPI) pada, tingkat pengontrol memori secara efektif. Yaitu, hapus aspek NUMA dari kode Anda dengan memiliki satu tugas per soket CPU atau pengontrol memori dan kemudian utas di bawah setiap tugas. Jika Anda melakukannya dengan cara itu, maka Anda harus dapat mengikat semua memori ke soket / pengontrol dengan aman baik melalui sentuhan pertama atau salah satu API yang tersedia, tidak peduli utas mana yang benar-benar melakukan pekerjaan alokasi atau inisialisasi. Pesan yang lewat di antara soket biasanya cukup optimal, setidaknya di MPI. Anda selalu dapat memiliki lebih banyak tugas MPI daripada ini, tetapi karena masalah yang Anda ajukan, saya jarang merekomendasikan orang untuk memiliki lebih sedikit.

Bill Barth
sumber
1
Ini adalah solusi praktis, tetapi meskipun kita dengan cepat mendapatkan lebih banyak core, jumlah core per NUMA cukup stagnan di sekitar 4. Jadi pada node 1000 core hipotetis, akankah kita menjalankan 250 proses MPI? (Ini akan bagus, tapi saya skeptis.)
Jed Brown
Saya tidak setuju bahwa jumlah core per NUMA stagnan. Sandy Bridge E5 memiliki 8. Magny Cours memiliki 12. Saya punya simpul Westmere-EX dengan 10. Interlagos (ORNL Titan) memiliki 20. Knights Corner akan memiliki lebih dari 50. Saya kira inti dari NUMA tetap dipertahankan. berpacu dengan Hukum Moore, lebih atau kurang.
Bill Barth
Magny Cours dan Interlagos memiliki dua mati di wilayah NUMA yang berbeda, sehingga 6 dan 8 inti per wilayah NUMA. Putar balik ke tahun 2006 di mana dua soket Clovertown quad-core akan berbagi antarmuka yang sama (chipset Blackford) ke memori dan tidak terlihat bagi saya seperti jumlah core per wilayah NUMA yang tumbuh begitu pesat. Blue Gene / Q memperluas pandangan datar dari memori ini sedikit lebih jauh dan mungkin Knight's Corner akan mengambil langkah lain (meskipun itu adalah perangkat yang berbeda, jadi mungkin kita harus membandingkannya dengan GPU, di mana kita memiliki 15 (Fermi) atau sekarang 8 ( Kepler) SM yang melihat memori datar).
Jed Brown
Panggilan bagus pada chip AMD. Saya sudah lupa. Namun, saya pikir Anda akan melihat pertumbuhan berkelanjutan di area ini untuk sementara waktu.
Bill Barth
6

Jawaban ini sebagai tanggapan atas dua kesalahpahaman terkait C ++ dalam pertanyaan.

  1. "Hal yang sama berlaku untuk operator baru C ++ yang bersikeras menginisialisasi alokasi baru (termasuk POD)"
  2. "Operator C ++ baru hanya membutuhkan satu parameter"

Ini bukan jawaban langsung untuk masalah multi-core yang Anda sebutkan. Hanya menanggapi komentar yang mengklasifikasikan programmer C ++ sebagai C ++ fanatik sehingga reputasi tetap terjaga;).

Untuk titik 1. C ++ "baru" atau alokasi stack tidak bersikeras menginisialisasi objek baru, apakah POD atau tidak. Konstruktor default kelas, seperti yang didefinisikan oleh pengguna, memiliki tanggung jawab itu. Kode pertama di bawah ini menunjukkan sampah yang dicetak apakah kelasnya POD atau tidak.

Ke poin 2. C ++ memungkinkan overloading "baru" dengan beberapa argumen. Kode kedua di bawah ini menunjukkan kasus seperti itu untuk mengalokasikan objek tunggal. Itu harus memberikan ide dan mungkin berguna untuk situasi yang Anda miliki. operator baru [] dapat dimodifikasi dengan tepat juga.

// Kode untuk poin 1.

#include <iostream>

struct A
{
    // int/double/char/etc not inited with 0
    // with or without this constructor
    // If present, the class is not POD, else it is.
    A() { }

    int i;
    double d;
    char c[20];
};

int main()
{
    A* a = new A;
    std::cout << a->i << ' ' << a->d << '\n';
    for(int i = 0; i < 20; ++i)
        std::cout << (int) a->c[i] << '\n';
}

Kompiler Intel 11.1 menunjukkan output ini (yang tentu saja memori tidak diinisialisasi yang ditunjukkan oleh "a").

993001483 6.50751e+029
105
108
... // skipped
97
108

// Kode untuk poin 2.

#include <cstddef>
#include <iostream>
#include <new>

// Just to use two different classes.
class arena { };
class policy { };

struct A
{
    void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
    {
        std::cout << "special operator new\n";
        return (void*)0x1234; //Just to test
    }
};

void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
{
    std::cout << "special operator new (global)\n";
    return (void*)0x5678; //Just to test
}

int main ()
{
    arena arena_obj;
    policy policy_obj;
    A* ptr = new(arena_obj, policy_obj) A;
    int* iptr = new(arena_obj, policy_obj) int;
    std::cout << ptr << "\n";
    std::cout << iptr << "\n";
}

sumber
Terima kasih atas koreksinya. Tampaknya C ++ tidak menghadirkan komplikasi tambahan relatif terhadap C, kecuali untuk array non-POD seperti std::complexyang secara eksplisit diinisialisasi.
Jed Brown
1
@JedBrown: Alasan nomor 6 untuk menghindari penggunaan std::complex?
Jack Poulson
1

Dalam kesepakatan. II kita punya infrastruktur perangkat lunak untuk memparalelkan perakitan pada setiap sel ke beberapa inti menggunakan Blok Bangunan Threading (pada dasarnya, Anda memiliki satu tugas per sel dan perlu menjadwalkan tugas-tugas ini ke prosesor yang tersedia - itu bukan bagaimana itu diimplementasikan tetapi itu adalah ide umum). Masalahnya adalah bahwa untuk integrasi lokal Anda memerlukan sejumlah objek sementara (awal) dan Anda harus menyediakan setidaknya sebanyak yang ada tugas yang dapat berjalan secara paralel. Kami melihat speedup yang buruk, mungkin karena ketika sebuah tugas diletakkan pada prosesor, ia mengambil salah satu objek awal yang biasanya akan berada di cache beberapa inti lainnya. Kami punya dua pertanyaan:

(i) Apakah ini benar-benar alasannya? Ketika kami menjalankan program di bawah cachegrind, saya melihat bahwa pada dasarnya saya menggunakan jumlah instruksi yang sama seperti ketika menjalankan program pada satu utas, namun total run-time yang diakumulasikan pada semua utas jauh lebih besar daripada yang satu-utas. Apakah ini benar-benar karena saya terus-menerus menyalahkan cache?

(ii) Bagaimana saya bisa mengetahui di mana saya berada, di mana masing-masing objek awal, dan objek awal mana yang harus saya ambil untuk mengakses yang panas di cache inti saya saat ini?

Pada akhirnya, kami belum menemukan jawaban untuk salah satu dari solusi ini dan setelah beberapa pekerjaan memutuskan bahwa kami tidak memiliki alat untuk menyelidiki dan menyelesaikan masalah ini. Saya tahu bagaimana setidaknya pada prinsipnya memecahkan masalah (ii) (yaitu, menggunakan objek thread-local, dengan asumsi bahwa thread tetap disematkan ke core prosesor - dugaan lain yang tidak mudah untuk diuji), tetapi saya tidak memiliki alat untuk menguji masalah (saya).

Jadi, dari sudut pandang kami, berurusan dengan NUMA masih merupakan pertanyaan yang belum terpecahkan.

Wolfgang Bangerth
sumber
Anda harus mengikat utas ke soket sehingga Anda tidak perlu bertanya-tanya apakah prosesor disematkan. Linux suka memindahkan barang.
Bill Barth
Selain itu, pengambilan sampel getcpu () atau sched_getcpu () (tergantung pada libc dan kernel Anda dan yang lainnya) akan memungkinkan Anda menentukan di mana thread berjalan di Linux.
Bill Barth
Ya, dan saya pikir Blok Bangunan Threading yang kami gunakan untuk menjadwalkan pekerjaan ke thread pin thread untuk prosesor. Inilah sebabnya kami mencoba bekerja dengan penyimpanan thread-lokal. Tetapi masih sulit bagi saya untuk menemukan solusi untuk masalah saya (i).
Wolfgang Bangerth
1

Di luar hwloc ada beberapa alat yang dapat melaporkan lingkungan memori cluster HPC dan yang dapat digunakan untuk mengatur berbagai konfigurasi NUMA.

Saya akan merekomendasikan LIKWID sebagai salah satu alat seperti itu menghindari pendekatan berbasis kode yang memungkinkan Anda misalnya untuk pin proses ke inti. Pendekatan perkakas untuk mengatasi konfigurasi memori khusus mesin ini akan membantu memastikan portabilitas kode Anda di seluruh cluster.

Anda dapat menemukan presentasi singkat yang menguraikannya dari ISC'13 " LIKWID - Alat Kinerja Ringan " dan penulis telah menerbitkan makalah tentang Arxiv " Praktik terbaik untuk rekayasa kinerja berbantuan HPM pada prosesor multicore modern ". Makalah ini menjelaskan pendekatan untuk menafsirkan data dari penghitung perangkat keras untuk mengembangkan kode performan khusus untuk arsitektur dan topologi memori mesin Anda.

eoinbrazil
sumber
LIKWID berguna, tetapi pertanyaannya lebih tentang bagaimana menulis perpustakaan numerik / memori-sensitif yang andal dapat memperoleh dan mengaudit lokalitas yang diharapkan di berbagai lingkungan eksekusi, skema threading, manajemen sumber daya MPI dan pengaturan afinitas, digunakan dengan perpustakaan lain, dll.
Jed Brown