แนวทางปฏิบัติที่ดีที่สุดในการจัดสรรหน่วยความจำแบบมัลติคอร์ / NUMA แบบพกพา / การกำหนดค่าเริ่มต้น


17

เมื่อทำการคำนวณแบนด์วิดท์หน่วยความจำที่ จำกัด จะดำเนินการในสภาพแวดล้อมหน่วยความจำที่ใช้ร่วมกัน (เช่นเธรดผ่าน OpenMP, Pthreads หรือ TBB) จะมีภาวะที่กลืนไม่เข้าคายไม่ออกของวิธีการตรวจสอบให้แน่ใจว่าหน่วยความจำกระจายอย่างถูกต้องผ่านหน่วยความจำกายภาพบัสหน่วยความจำ "ท้องถิ่น" แม้ว่าอินเทอร์เฟซนั้นไม่สามารถพกพาได้ระบบปฏิบัติการส่วนใหญ่มีวิธีตั้งค่าความสัมพันธ์ของเธรด (เช่นpthread_setaffinity_np()ในระบบ POSIX จำนวนมากsched_setaffinity()บน Linux SetThreadAffinityMask()บน Windows) นอกจากนี้ยังมีไลบรารีเช่นhwlocสำหรับกำหนดลำดับชั้นของหน่วยความจำ แต่น่าเสียดายที่ระบบปฏิบัติการส่วนใหญ่ยังไม่ได้เตรียมวิธีในการตั้งค่านโยบายหน่วยความจำ NUMA Linux เป็นข้อยกเว้นที่น่าทึ่งด้วยlibnumaการอนุญาตให้แอปพลิเคชันจัดการนโยบายหน่วยความจำและการโยกย้ายหน้าเว็บที่หน้าย่อย (ตั้งแต่เดือนพ. ศ. 2547 เป็นต้นมาซึ่งมีอยู่ทั่วไป) ระบบปฏิบัติการอื่นคาดว่าผู้ใช้จะปฏิบัติตามนโยบาย "สัมผัสแรก" โดยนัย

การทำงานกับนโยบาย "สัมผัสแรก" หมายความว่าผู้โทรควรสร้างและแจกจ่ายเธรดด้วยความสัมพันธ์ที่พวกเขาวางแผนที่จะใช้ในภายหลังเมื่อเขียนไปยังหน่วยความจำที่จัดสรรใหม่ครั้งแรก (ระบบน้อยมากที่มีการกำหนดค่าเช่นที่malloc()พบหน้าจริง ๆ มันแค่สัญญาว่าจะพบพวกเขาเมื่อพวกเขาจะผิดจริงอาจจะโดยกระทู้ต่าง ๆ ) นี่ก็หมายความว่าการจัดสรรการใช้calloc()หรือการเริ่มต้นทันทีหลังจากการจัดสรรหน่วยความจำใช้memset()เป็นอันตราย หน่วยความจำทั้งหมดบนบัสหน่วยความจำของคอร์ที่รันเธรดที่จัดสรรซึ่งนำไปสู่แบนด์วิดท์หน่วยความจำกรณีที่แย่ที่สุดเมื่อเข้าถึงหน่วยความจำจากหลายเธรด เช่นเดียวกับตัวดำเนินการ C ++ newซึ่งยืนยันในการเริ่มต้นการจัดสรรใหม่จำนวนมาก (เช่นstd::complex) ข้อสังเกตบางประการเกี่ยวกับสภาพแวดล้อมนี้:

  • การจัดสรรสามารถทำ "กลุ่มรวม" แต่ตอนนี้การจัดสรรกลายเป็นผสมในรูปแบบเกลียวซึ่งเป็นที่ไม่พึงประสงค์สำหรับห้องสมุดซึ่งอาจต้องโต้ตอบกับลูกค้าโดยใช้รูปแบบเกลียวที่แตกต่างกัน (อาจแต่ละคนมีเธรดพูลของตัวเอง)
  • RAII ถือเป็นส่วนสำคัญของสำนวน C ++ แต่ดูเหมือนว่าจะเป็นอันตรายต่อประสิทธิภาพหน่วยความจำในสภาพแวดล้อม NUMA ตำแหน่งnewสามารถใช้กับหน่วยความจำที่จัดสรรผ่านmalloc()หรือตามปกติlibnumaแต่จะเปลี่ยนกระบวนการจัดสรร (ซึ่งฉันเชื่อว่าจำเป็น)
  • แก้ไข: คำสั่งก่อนหน้าของฉันเกี่ยวกับผู้ประกอบการnewไม่ถูกต้องมันสามารถรองรับอาร์กิวเมนต์หลายข้อดูคำตอบของ Chetan ฉันเชื่อว่ายังคงมีความกังวลในการรับไลบรารีหรือคอนเทนเนอร์ STL เพื่อใช้ความสัมพันธ์ที่ระบุ อาจมีการบรรจุหลายฟิลด์และอาจไม่สะดวกเพื่อให้แน่ใจว่าเช่นการstd::vectorจัดสรรใหม่โดยใช้ตัวจัดการบริบทที่ถูกต้องใช้งานอยู่
  • แต่ละเธรดสามารถจัดสรรและข้อผิดพลาดของหน่วยความจำส่วนตัวของตนเอง แต่การทำดัชนีในพื้นที่ใกล้เคียงนั้นมีความซับซ้อนมากขึ้น (พิจารณาเมทริกซ์กระจัดกระจาย - เวกเตอร์ผลิตภัณฑ์กับพาร์ทิชันแถวของเมทริกซ์และเวกเตอร์การทำดัชนีส่วนที่ไม่ได้เป็นเจ้าของของต้องการโครงสร้างข้อมูลที่ซับซ้อนมากขึ้นเมื่อไม่ต่อเนื่องกันในหน่วยความจำเสมือน)YAxxx

โซลูชันใด ๆ สำหรับการจัดสรร / การกำหนดค่าเริ่มต้นของ NUMA ถือว่าเป็นคำ ฉันเคยออกไป gotchas ที่สำคัญอื่น ๆ ?

(ผมไม่ได้หมายสำหรับฉัน C ++ ตัวอย่างที่จะบ่งบอกความสำคัญกับภาษานั้น แต่ที่ C ++ ภาษา encodes การตัดสินใจบางอย่างเกี่ยวกับการจัดการหน่วยความจำที่เป็นภาษาเช่น C ไม่ได้จึงมีแนวโน้มที่จะมีความต้านทานมากขึ้นเมื่อบอกว่า C ++ โปรแกรมเมอร์ทำเหล่านั้น สิ่งที่แตกต่างกัน)

คำตอบ:


7

ทางออกหนึ่งสำหรับปัญหานี้ที่ฉันมักจะชอบคือการแยกเธรดและงาน (MPI) ที่ระดับคอนโทรลเลอร์หน่วยความจำอย่างมีประสิทธิภาพ เช่นลบ NUMA ด้านจากรหัสของคุณโดยมีหนึ่งงานต่อซ็อกเก็ตซีพียูหรือตัวควบคุมหน่วยความจำแล้วหัวข้อภายใต้แต่ละงาน หากคุณทำเช่นนั้นคุณควรผูกหน่วยความจำทั้งหมดกับซ็อกเก็ต / คอนโทรลเลอร์นั้นอย่างปลอดภัยไม่ว่าจะผ่านการสัมผัสครั้งแรกหรือหนึ่งใน API ที่พร้อมใช้งานไม่ว่าเธรดใดจะทำงานของการจัดสรรหรือการเริ่มต้นจริง ๆ ข้อความที่ผ่านระหว่างซ็อกเก็ตมักจะได้รับการปรับปรุงให้ดีที่สุดใน MPI อย่างน้อยที่สุด คุณสามารถมีงาน MPI ได้มากกว่านี้ แต่เนื่องจากปัญหาที่คุณเพิ่มฉันไม่ค่อยแนะนำให้คนมีน้อย


1
นี่เป็นวิธีแก้ปัญหาที่ใช้งานได้จริงแม้ว่าเราจะได้รับแกนประมวลผลเพิ่มขึ้นอย่างรวดเร็ว แต่จำนวนแกนต่อโหนด NUMA นั้นค่อนข้างนิ่งที่ประมาณ 4 ดังนั้นในสมมุติฐาน 1,000 คอร์แกนเราจะใช้กระบวนการ 250 MPI หรือไม่ (มันจะดี แต่ฉันไม่เชื่อ)
Jed Brown

ฉันไม่เห็นด้วยที่จำนวนแกนต่อ NUMA นั้นนิ่ง Sandy Bridge E5 มี 8. Magny Cours มี 12. ฉันมีโหนด Westmere-EX ที่มี 10 Interlagos (ORNL Titan) มี 20. Knights Corner จะมีมากกว่า 50 ฉันเดาว่าแกนต่อ NUMA นั้นยังคงอยู่ ก้าวตามกฎของมัวร์ไม่มากก็น้อย
Bill Barth

Magny Cours และ Interlagos มีผู้เสียชีวิตสองรายในภูมิภาค NUMA ที่แตกต่างกันดังนั้น 6 และ 8 คอร์ต่อภูมิภาค NUMA ย้อนกลับไปในปี 2549 ที่ซ็อคเวิร์ตแบบ quad-core สองซ็อกเก็ตจะแบ่งปันอินเทอร์เฟซเดียวกัน (ชิปเซ็ตแบล็กฟอร์ด) กับหน่วยความจำและมันก็ดูไม่เหมือนฉันเช่นจำนวนแกนหลักต่อภูมิภาค NUMA Blue Gene / Q ขยายมุมมองแบบแบนของหน่วยความจำอีกเล็กน้อยและบางที Knight's Corner จะใช้ขั้นตอนอื่น (แม้ว่ามันจะเป็นอุปกรณ์ที่แตกต่างกันดังนั้นบางทีเราควรจะเปรียบเทียบกับ GPU แทนซึ่งเรามี 15 (Fermi) หรือตอนนี้ 8 ( Kepler) SM กำลังดูหน่วยความจำแบบแบน)
Jed Brown

โทรดีกับชิป AMD ฉันลืมไปแล้ว ถึงกระนั้นฉันคิดว่าคุณจะเห็นการเติบโตอย่างต่อเนื่องในพื้นที่นี้ชั่วครู่
Bill Barth

6

คำตอบนี้เป็นการตอบสนองต่อความเข้าใจผิดที่เกี่ยวข้องกับ C ++ สองประการ

  1. "เช่นเดียวกับตัวดำเนินการใหม่ C ++ ซึ่งยืนยันในการเริ่มต้นการจัดสรรใหม่ (รวมถึง POD)"
  2. "ตัวดำเนินการ C ++ ใหม่ใช้พารามิเตอร์เดียวเท่านั้น"

ไม่ใช่คำตอบโดยตรงสำหรับปัญหาแบบมัลติคอร์ที่คุณพูดถึง เพียงแค่ตอบกลับความคิดเห็นที่จัดประเภทโปรแกรมเมอร์ C ++ เป็น zealots C ++ เพื่อให้ชื่อเสียงยังคงอยู่;)

ไปยังจุดที่ 1 C ++ "ใหม่" หรือการจัดสรรสแต็คไม่ยืนยันในการเริ่มต้นวัตถุใหม่ไม่ว่าจะเป็น POD หรือไม่ ตัวสร้างเริ่มต้นของคลาสตามที่กำหนดโดยผู้ใช้มีความรับผิดชอบนั้น รหัสแรกด้านล่างแสดงขยะที่พิมพ์ว่าคลาสเป็น POD หรือไม่

ถึงจุดที่ 2 C ++ อนุญาตให้โหลดมากเกินไป "ใหม่" ที่มีหลายอาร์กิวเมนต์ รหัสที่สองด้านล่างแสดงกรณีเช่นนี้สำหรับการจัดสรรวัตถุเดี่ยว ควรให้ความคิดและอาจเป็นประโยชน์สำหรับสถานการณ์ที่คุณมี ตัวดำเนินการใหม่ [] สามารถปรับเปลี่ยนได้อย่างเหมาะสมเช่นกัน

// รหัสสำหรับจุด 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';
}

คอมไพเลอร์ 11.1 ของ Intel แสดงผลลัพธ์นี้ (ซึ่งแน่นอนว่าเป็นหน่วยความจำที่ไม่ได้กำหนดค่าเริ่มต้นโดย "a")

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

// รหัสสำหรับจุด 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";
}

ขอบคุณสำหรับการแก้ไข ดูเหมือนว่า C ++ จะไม่แสดงภาวะแทรกซ้อนเพิ่มเติมที่เกี่ยวข้องกับ C ยกเว้นสำหรับอาร์เรย์ที่ไม่ใช่ POD เช่นstd::complexซึ่งถูกเตรียมใช้งานอย่างชัดเจน
Jed Brown

1
@JedBrown: เหตุผลที่ 6 เพื่อหลีกเลี่ยงการใช้std::complex?
Jack Poulson

1

ในการจัดการ II เรามีโครงสร้างพื้นฐานซอฟต์แวร์ที่จะทำให้การประกอบในแต่ละเซลล์บนหลายคอร์โดยใช้ Threading Building Blocks (ในสาระสำคัญคุณมีงานหนึ่งรายการต่อเซลล์และจำเป็นต้องกำหนดเวลางานเหล่านี้ลงในโปรเซสเซอร์ที่มีให้ - นั่นไม่ใช่วิธี ดำเนินการแล้ว แต่เป็นแนวคิดทั่วไป) ปัญหาคือว่าสำหรับการรวมในท้องถิ่นคุณต้องมีวัตถุชั่วคราว (เริ่มต้น) จำนวนหนึ่งและคุณต้องจัดเตรียมอย่างน้อยที่สุดเท่าที่มีงานที่สามารถทำงานแบบขนาน เราเห็นการเร่งความเร็วที่ไม่ดีอย่างน่าจะเป็นเพราะเมื่องานถูกวางลงบนโปรเซสเซอร์มันจะคว้าวัตถุรอยขีดข่วนอันหนึ่งซึ่งโดยทั่วไปจะอยู่ในแคชของคอร์อื่น เรามีสองคำถาม:

(i) นี่คือเหตุผลจริงๆหรือ เมื่อเรารันโปรแกรมภายใต้ cachegrind ฉันเห็นว่าฉันใช้คำสั่งจำนวนเท่าเดิมเหมือนกับตอนที่รันโปรแกรมบนเธรดเดี่ยว แต่จำนวนรันไทม์ทั้งหมดที่สะสมบนเธรดทั้งหมดนั้นใหญ่กว่าเธรดเดี่ยวมาก เป็นเพราะฉันทำผิดแคชอย่างต่อเนื่องหรือไม่

(ii) ฉันจะทราบได้อย่างไรว่าฉันอยู่ที่ไหนที่ใดที่วัตถุรอยขีดข่วนแต่ละชิ้นและวัตถุรอยขีดข่วนที่ฉันต้องใช้เพื่อเข้าถึงวัตถุที่กำลังร้อนในแคชของแกนปัจจุบันของฉัน

ท้ายที่สุดเราไม่พบคำตอบสำหรับวิธีแก้ไขปัญหาเหล่านี้และหลังจากงานสองสามตัดสินใจว่าเราไม่มีเครื่องมือในการตรวจสอบและแก้ไขปัญหาเหล่านี้ ฉันรู้วิธีการอย่างน้อยในหลักการแก้ปัญหา (ii) (กล่าวคือการใช้วัตถุเธรดในท้องถิ่นโดยสมมติว่าเธรดยังคงถูกตรึงอยู่กับแกนประมวลผล - การคาดเดาอีกครั้งที่ไม่ทดสอบเล็กน้อย) แต่ฉันไม่มีเครื่องมือในการทดสอบปัญหา (ผม).

ดังนั้นจากมุมมองของเราการจัดการกับ NUMA ยังคงเป็นคำถามที่ยังไม่แก้


คุณควรผูกเธรดของคุณเข้ากับซ็อกเก็ตเพื่อที่คุณจะได้ไม่ต้องสงสัยว่าโปรเซสเซอร์ถูกตรึงหรือไม่ ลินุกซ์ชอบที่จะย้ายสิ่งต่าง ๆ
Bill Barth

นอกจากนี้การสุ่มตัวอย่าง getcpu () หรือ sched_getcpu () (ขึ้นอยู่กับ libc และเคอร์เนลของคุณและ whatnot) ควรอนุญาตให้คุณกำหนดตำแหน่งที่เธรดกำลังทำงานบน Linux
Bill Barth

ใช่และฉันคิดว่า Threading Building Block ที่เราใช้เพื่อกำหนดเวลาการทำงานลงบนเธรดพินเธรดให้กับโปรเซสเซอร์ นี่คือเหตุผลที่เราพยายามทำงานกับที่จัดเก็บเธรดโลคัล แต่ก็ยังยากที่ฉันจะหาวิธีแก้ปัญหาของฉัน (i)
Wolfgang Bangerth

1

นอกเหนือจาก hwloc มีเครื่องมือบางอย่างที่สามารถรายงานเกี่ยวกับสภาพแวดล้อมหน่วยความจำของคลัสเตอร์ HPC และสามารถใช้เพื่อตั้งค่าการกำหนดค่า NUMA ได้หลากหลาย

ฉันอยากจะแนะนำ LIKWID เป็นหนึ่งในเครื่องมือดังกล่าวเพราะมันหลีกเลี่ยงวิธีการที่ใช้รหัสช่วยให้คุณยกตัวอย่างเช่นการปักหมุดกระบวนการเป็นหลัก วิธีการของเครื่องมือในการจัดการกับการกำหนดค่าหน่วยความจำเฉพาะของเครื่องนี้จะช่วยให้มั่นใจได้ถึงความสามารถในการพกพาของรหัสของคุณทั่วทั้งคลัสเตอร์

คุณสามารถหางานนำเสนอสั้น ๆ ได้จาก ISC'13 " LIKWID - เครื่องมือวัดประสิทธิภาพที่มีน้ำหนักเบา " และผู้เขียนได้ตีพิมพ์บทความเกี่ยวกับ Arxiv " แนวทางปฏิบัติที่ดีที่สุดสำหรับวิศวกรรมประสิทธิภาพของ HPM บนโปรเซสเซอร์มัลติคอร์ที่ทันสมัย " บทความนี้อธิบายวิธีการตีความข้อมูลจากตัวนับฮาร์ดแวร์เพื่อพัฒนารหัสตัวแสดงเฉพาะกับสถาปัตยกรรมและโครงสร้างหน่วยความจำของเครื่องของคุณ


LIKWID มีประโยชน์ แต่คำถามคือเพิ่มเติมเกี่ยวกับวิธีการเขียนไลบรารี่ตัวเลข / หน่วยความจำที่ไวต่อความน่าเชื่อถือที่สามารถรับและตรวจสอบด้วยตนเองในท้องถิ่นที่คาดหวังในสภาพแวดล้อมการดำเนินการที่หลากหลาย ห้องสมุดอื่น ๆ ฯลฯ
Jed Brown
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.