Taşınabilir çok çekirdekli / NUMA bellek ayırma / başlatma en iyi uygulamaları


17

Bellek bant genişliği sınırlı hesaplamalar paylaşılan bellek ortamlarında (ör. OpenMP, Pthreads veya TBB aracılığıyla iş parçacığı) gerçekleştirildiğinde, belleğin fiziksel bellekte doğru bir şekilde dağıtılmasının nasıl sağlanacağına dair bir ikilem vardır; "yerel" bellek veri yolu. Arabirimler taşınabilir olmasa da, çoğu işletim sisteminin iş parçacığı benzeşimini ayarlama yolları vardır (örneğin pthread_setaffinity_np()birçok POSIX sisteminde, sched_setaffinity()Linux'ta, SetThreadAffinityMask()Windows'ta). Bellek hiyerarşisini belirlemek için hwloc gibi kütüphaneler de vardır , ancak maalesef çoğu işletim sistemi henüz NUMA bellek politikalarını ayarlamak için yollar sağlamamaktadır. Linux, libnuma ile dikkate değer bir istisnadıruygulamanın bellek politikasını ve sayfa geçişini sayfa ayrıntı düzeyinde değiştirmesine olanak tanır (2004'ten bu yana ana hattadır, dolayısıyla yaygın olarak bulunur). Diğer işletim sistemleri, kullanıcıların örtük bir "ilk dokunma" politikası izlemesini bekler.

"İlk dokunma" ilkesi ile çalışmak, arayanın yeni ayrılan belleğe ilk yazarken daha sonra kullanmayı planladığı yakınlıkla ileti oluşturması ve dağıtması gerektiği anlamına gelir. (Çok az sistemleri şekilde yapılandırılmış malloc()sadece gerçekte hatalı olduğunda farklı iş parçacıkları tarafından belki de onları bulmak için vaat, aslında sayfaları bulur.) Bu kullanarak bu tahsisi ima calloc()veya hemen kullanarak tahsisi sonrasında hafızasını başlatılıyor memset()o arıza eğiliminde olacaktır çünkü zararlıdır tüm belleği, ayırma iş parçacığını çalıştıran çekirdeğin bellek veri yoluna bağlayarak, belleğe birden çok iş parçacığından erişildiğinde en kötü bellek bant genişliğine yol açar. Aynı şey newbirçok yeni tahsinin başlatılmasında ısrar eden C ++ operatörü için de geçerlidir (ör.std::complex). Bu çevre ile ilgili bazı gözlemler:

  • Ayırma "iş parçacığı toplu" olarak yapılabilir, ancak şimdi ayırma, farklı iş parçacığı modelleri (belki de her biri kendi iş parçacığı havuzları olan) kullanarak istemcilerle etkileşim kurmak zorunda kalabilen kitaplıklar için istenmeyen iş parçacığı modeline karıştırılır.
  • RAII, idiyomatik C ++ 'ın önemli bir parçası olarak kabul edilir, ancak bir NUMA ortamında bellek performansı için aktif olarak zararlı gibi görünüyor. Yerleşim new, ayrılan bellek malloc()veya rutinlerle kullanılabilir libnuma, ancak bu ayırma işlemini değiştirir (ki bu gerekli olduğuna inanıyorum).
  • DÜZENLEME: İşleç hakkındaki önceki ifadem newyanlıştı, birden çok argümanı destekleyebilir, Chetan'ın cevabına bakın. Hala belirtilen afiniteyi kullanmak için kütüphanelerin veya STL kaplarının elde edilmesiyle ilgili bir endişe olduğuna inanıyorum. Birden çok alan paketlenmiş olabilir ve örneğin std::vectordoğru bağlam yöneticisi etkin olarak yeniden tahsis edilmesini sağlamak uygun olmayabilir .
  • Her iş parçacığı kendi özel belleğini ayırabilir ve bozabilir, ancak daha sonra komşu bölgelere endeksleme yapmak daha karmaşıktır. (Seyrek bir matris vektör ürün dikkate matris ve vektörlerinin bir satır, bölüm ile, bir sahipsiz bir kısmını indeksleme x , daha karmaşık veri yapısı gerektirmektedir x . Sanal bellek bitişik değildir)ybirxxx

NUMA tahsisi / başlatmaya yönelik çözümlerin deyimsel olduğu düşünülüyor mu? Diğer kritik gotcha'ları da bıraktım mı?

(C ++ örneklerimin bu dile vurgu yaptığı anlamına gelmez, ancak C ++ dili , C gibi bir dilin yapmadığı bellek yönetimi hakkında bazı kararları kodlar, bu nedenle C ++ programcılarının bunları yapmasını önerirken daha fazla direnç eğilimi farklı şeyler.)

Yanıtlar:


7

Tercih ettiğim bu soruna bir çözüm, iş parçacığı ve (MPI) görevlerini etkin biçimde bellek denetleyicisi düzeyinde ayrıştırmaktır. Yani, CPU soketi veya bellek denetleyicisi başına bir görev ve ardından her görevin altındaki iş parçacıkları ile NUMA yönlerini kodunuzdan kaldırın. Bunu bu şekilde yaparsanız, tahsis veya başlatma işi ne olursa olsun, ilk dokunuşla veya kullanılabilir API'lerden biri ile tüm belleği bu sokete / denetleyiciye güvenli bir şekilde bağlayabilmeniz gerekir. Soketler arasında geçen mesaj genellikle en azından MPI'da oldukça iyi optimize edilmiştir. Her zaman bundan daha fazla ÇBYE görevine sahip olabilirsiniz, ancak gündeme getirdiğiniz sorunlar nedeniyle, nadiren milletinizin daha az olmasını öneriyorum.


1
Bu pratik bir çözümdür, ancak hızla daha fazla çekirdek alsak da, NUMA düğümü başına çekirdek sayısı 4 civarında oldukça durgundur. Öyleyse varsayımsal 1000 çekirdek düğümde 250 MPI işlemi gerçekleştirecek miyiz? (Bu harika olurdu, ama şüpheliyim.)
Jed Brown

NUMA başına çekirdek sayısının durağan olduğunu kabul etmiyorum. Sandy Bridge E5'in 8'i var. Magny Cours'un 12'si vardı. 10'lu bir Westmere-EX düğümü var. Interlagos (ORNL Titan) 20'si var. Şövalyeler Köşesi 50'den fazla olacak. Moore Yasası'na az çok.
Bill Barth

Magny Cours ve Interlagos'un farklı NUMA bölgelerinde iki ölümü vardır, bu nedenle NUMA bölgesi başına 6 ve 8 çekirdeğe sahiptir. Dört çekirdekli Clovertown'un iki yuvasının aynı arabirimi (Blackford yonga seti) belleğe paylaşacağı 2006'ya geri dönün ve bana NUMA bölgesi başına çekirdek sayısının çok hızlı arttığı gibi görünmüyor. Blue Gene / Q, hafızanın bu düz görünümünü biraz daha genişletir ve belki de Knight's Corner başka bir adım atacaktır (farklı bir cihaz olmasına rağmen, belki de 15 (Fermi) veya şimdi 8 (8) olan GPU'larla karşılaştırmalıyız. Kepler) Düz bellek görüntüleyen SM'ler).
Jed Brown

AMD yongalarına iyi çağrı. Unuttum. Yine de, bir süre bu alanda sürekli büyüme göreceğinizi düşünüyorum.
Bill Barth

6

Bu cevap, sorudaki C ++ ile ilgili iki yanlış anlama cevap vermektedir.

  1. "Aynısı, yeni tahsislerin başlatılmasında ısrar eden C ++ yeni operatörü için de geçerlidir (POD'lar dahil)"
  2. "C ++ operatörü yeni yalnızca bir parametre alır"

Bahsettiğiniz çok çekirdekli sorunlara doğrudan bir cevap değildir. Sadece C ++ programcılarını C ++ zealots olarak sınıflandıran yorumlara yanıt vererek itibarını korur;).

1. noktaya. C ++ "yeni" veya yığın tahsisi, ister POD olsun ister olmasın yeni nesnelerin başlatılmasında ısrar etmez. Sınıfın kullanıcı tarafından tanımlanan varsayılan kurucusu bu sorumluluğa sahiptir. Aşağıdaki ilk kod, sınıfın POD olsun ya da olmasın, basılı yazdırmayı gösterir.

Nokta 2. C ++, "yeni" birden çok bağımsız değişken ile aşırı yüklenmesine izin verir. Aşağıdaki ikinci kod, tek nesnelerin tahsis edilmesi için böyle bir durumu göstermektedir. Bir fikir vermeli ve belki de sahip olduğunuz durum için yararlı olacaktır. operatör yeni [] de uygun şekilde değiştirilebilir.

// 1. nokta için kod.

#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';
}

Intel'in 11.1 derleyicisi bu çıkışı gösterir (ki bu elbette "a" ile gösterilen başlatılmamış bellektir).

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

// 2. nokta için kod.

#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";
}

Düzeltmeler için teşekkürler. C ++ gibi POD olmayan dizileri hariç C göre değil, bu ek komplikasyonlar, yapar görünüyor std::complexki edilir açıkça başlatıldı.
Jed Brown

1
@JedBrown: Kullanmamak için 6 numaralı neden std::complex?
Jack Poulson

1

II. İş Parçacığı Yapı Taşlarını kullanarak her bir hücredeki montajı birden çok çekirdeğe paralel hale getirmek için yazılım altyapımız var (özünde, hücre başına bir göreviniz var ve bu görevleri kullanılabilir işlemcilere programlamanız gerekiyor. ama genel fikir). Sorun, yerel entegrasyon için bir dizi geçici (çizik) nesneye ihtiyaç duymanız ve en az paralel olarak çalışabilecek görevler sağlamanız gerektiğidir. Muhtemelen hızlanma görüyoruz, çünkü bir işlem bir işlemciye konduğunda, genellikle başka bir çekirdeğin önbelleğinde olacak çizik nesnelerinden birini yakalar. İki sorumuz vardı:

(i) Nedeni bu mu? Programı cachegrind altında çalıştırdığımızda, programı tek bir iş parçacığında çalıştırırken temel olarak aynı sayıda talimat kullandığımı görüyorum, ancak tüm iş parçacıkları üzerinde biriken toplam çalışma süresi tek iş parçacığından çok daha büyük. Gerçekten sürekli önbellek hata çünkü?

(ii) Nerede olduğumu, çizik nesnelerinin her birinin nerede olduğunu ve mevcut çekirdeğimin önbelleğinde sıcak olana erişmek için hangi çizik nesnesini bulmam gerektiğini nasıl öğrenebilirim?

Nihayetinde, bu çözümlerin hiçbirine cevap bulamadık ve birkaç çalışmadan sonra bu sorunları araştırmak ve çözmek için araçlardan yoksun olduğumuza karar verdik. En azından prensipte problemi (ii) nasıl çözeceğimizi biliyorum (yani, evre çekirdeklerine sabitlenmiş kaldığı varsayılarak, yerel-yerel nesneleri kullanarak - test etmek için önemsiz olmayan başka bir varsayım), ancak sorunu test etmek için hiçbir aracım yok (ben).

Yani, bizim açımızdan, NUMA ile uğraşmak hala çözülmemiş bir sorudur.


İşlemcilerin sabitlenip sabitlenmediğini merak etmenize gerek kalmaması için dişlerinizi yuvalara bağlamanız gerekir. Linux bir şeyleri taşımayı sever.
Bill Barth

Ayrıca, getcpu () veya sched_getcpu () örneklemesi (libc ve çekirdeğinize ve ne olduğuna bağlı olarak) Linux'ta iş parçacıklarının nerede çalıştığını belirlemenize izin vermelidir.
Bill Barth

Evet, ve iş parçacığı iğneler iş parçacıkları işlemciler işlemek için zamanlama kullandığımız diş yapı taşları düşünüyorum. Bu nedenle yerel iş parçacığı depolama ile çalışmaya çalıştık. Ama sorunuma bir çözüm bulmak benim için hala zor (i).
Wolfgang Bangerth

1

Hwloc'un ötesinde, bir HPC kümesinin bellek ortamı hakkında rapor verebilen ve çeşitli NUMA yapılandırmalarını ayarlamak için kullanılabilecek birkaç araç vardır.

Örneğin bir süreci bir çekirdeğe sabitlemenizi sağlayan kod tabanlı bir yaklaşımdan kaçındığı için LIKWID'i böyle bir araç olarak öneriyorum. Makineye özgü bellek yapılandırmasına yönelik bu araç yaklaşımı, kodunuzun kümeler arasında taşınabilirliğini sağlamaya yardımcı olacaktır.

ISC'13 " LIKWID - Hafif Performans Araçları " ndan kısa bir sunum bulabilirsiniz ve yazarlar Arxiv " Modern çok çekirdekli işlemcilerde HPM destekli performans mühendisliği için en iyi uygulamalar " konulu bir makale yayınladılar . Bu makalede, makinenizin mimarisine ve bellek topolojisine özgü performans kodu geliştirmek için donanım sayaçlarındaki verilerin yorumlanmasına yönelik bir yaklaşım açıklanmaktadır.


LIKWID faydalıdır, ancak soru daha çok çeşitli yürütme ortamlarında, iş parçacığı şemaları, MPI kaynak yönetimi ve yakınlık ayarı, diğer kütüphaneler, vb.
Jed Brown
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.