Thực hành tốt nhất phân bổ / khởi tạo bộ nhớ đa điểm / NUMA


17

Khi các tính toán giới hạn băng thông bộ nhớ được thực hiện trong môi trường bộ nhớ dùng chung (ví dụ: luồng qua OpenMP, Pthreads hoặc TBB), có một vấn đề nan giải là làm thế nào để đảm bảo rằng bộ nhớ được phân phối chính xác trên bộ nhớ vật lý , sao cho mỗi luồng chủ yếu truy cập bộ nhớ trên một Xe buýt bộ nhớ "cục bộ". Mặc dù các giao diện không khả dụng, nhưng hầu hết các hệ điều hành đều có cách đặt mối quan hệ luồng (ví dụ: pthread_setaffinity_np()trên nhiều hệ thống POSIX, sched_setaffinity()trên Linux, SetThreadAffinityMask()trên Windows). Ngoài ra còn có các thư viện như hwloc để xác định hệ thống phân cấp bộ nhớ, nhưng thật không may, hầu hết các hệ điều hành chưa cung cấp cách để đặt chính sách bộ nhớ NUMA. Linux là một ngoại lệ đáng chú ý, với libnumacho phép ứng dụng thao tác chính sách bộ nhớ và di chuyển trang ở mức độ chi tiết của trang (theo dòng chính từ năm 2004, do đó có sẵn rộng rãi). Các hệ điều hành khác mong muốn người dùng tuân thủ chính sách "chạm đầu tiên" ngầm định.

Làm việc với chính sách "chạm đầu tiên" có nghĩa là người gọi nên tạo và phân phối các luồng với bất kỳ mối quan hệ nào họ dự định sử dụng sau này khi lần đầu tiên ghi vào bộ nhớ được cấp phát mới. (Rất ít hệ thống được cấu hình sao cho malloc()thực sự tìm thấy các trang, nó chỉ hứa sẽ tìm thấy chúng khi chúng thực sự bị lỗi, có lẽ bởi các luồng khác nhau.) Điều này ngụ ý rằng việc cấp phát sử dụng calloc()hoặc khởi tạo ngay bộ nhớ sau khi phân bổ sử dụng memset()là có hại vì nó sẽ có lỗi tất cả bộ nhớ trên bus bộ nhớ của lõi chạy luồng cấp phát, dẫn đến băng thông bộ nhớ trong trường hợp xấu nhất khi bộ nhớ được truy cập từ nhiều luồng. Điều tương tự cũng áp dụng cho newtoán tử C ++ , đòi hỏi phải khởi tạo nhiều phân bổ mới (ví dụ:std::complex). Một số quan sát về môi trường này:

  • Phân bổ có thể được tạo thành "tập thể luồng", nhưng giờ phân bổ trở thành hỗn hợp vào mô hình luồng, điều không mong muốn đối với các thư viện có thể phải tương tác với các máy khách bằng cách sử dụng các mô hình luồng khác nhau (có lẽ mỗi nhóm có nhóm luồng riêng).
  • RAII được coi là một phần quan trọng của C ++ thành ngữ, nhưng dường như nó có hại tích cực cho hiệu năng bộ nhớ trong môi trường NUMA. Vị trí newcó thể được sử dụng với bộ nhớ được phân bổ thông qua malloc()hoặc thường xuyên từ libnuma, nhưng điều này thay đổi quá trình phân bổ (mà tôi tin là cần thiết).
  • EDIT: Tuyên bố trước đây của tôi về toán tử newlà không chính xác, nó có thể hỗ trợ nhiều đối số, xem câu trả lời của Chetan. Tôi tin rằng vẫn còn lo ngại về việc thư viện hoặc bộ chứa STL sử dụng mối quan hệ được chỉ định. Nhiều trường có thể được đóng gói và có thể bất tiện để đảm bảo rằng, ví dụ, một std::vectorphân bổ lại với trình quản lý bối cảnh chính xác đang hoạt động.
  • Mỗi luồng có thể phân bổ và lỗi bộ nhớ riêng của nó, nhưng sau đó lập chỉ mục vào các vùng lân cận thì phức tạp hơn. (Xem xét một sản phẩm vectơ ma trận thưa thớt với phân vùng hàng của ma trận và vectơ; lập chỉ mục phần chưa được đặt của x yêu cầu cấu trúc dữ liệu phức tạp hơn khi x không liền kề trong bộ nhớ ảo.)yMộtxxx

Có bất kỳ giải pháp nào để phân bổ / khởi tạo NUMA được coi là thành ngữ không? Tôi đã bỏ đi các vấn đề quan trọng khác chưa?

(Tôi không có ý cho tôi C ++ ví dụ để ngụ ý một sự nhấn mạnh về ngôn ngữ đó, tuy nhiên C ++ ngôn ngữ mã hóa một số quyết định về quản lý bộ nhớ rằng một ngôn ngữ như C không, do đó có xu hướng có sức đề kháng hơn khi gợi ý rằng C lập trình viên ++ làm những những thứ khác nhau.)

Câu trả lời:


7

Một giải pháp cho vấn đề này mà tôi có xu hướng thích là phân tách các luồng và các tác vụ (MPI) ở cấp độ bộ điều khiển bộ nhớ một cách hiệu quả. Tức là, loại bỏ các khía cạnh NUMA khỏi mã của bạn bằng cách có một tác vụ trên mỗi ổ cắm CPU hoặc bộ điều khiển bộ nhớ và sau đó xử lý các luồng trong mỗi tác vụ. Nếu bạn làm theo cách đó, thì bạn sẽ có thể liên kết tất cả bộ nhớ với ổ cắm / bộ điều khiển đó một cách an toàn thông qua lần chạm đầu tiên hoặc một trong các API có sẵn cho dù luồng nào thực sự thực hiện công việc cấp phát hoặc khởi tạo. Thông điệp chuyển giữa các ổ cắm thường được tối ưu hóa khá tốt, ít nhất là bằng MPI. Bạn luôn có thể có nhiều nhiệm vụ MPI hơn thế này, nhưng do các vấn đề bạn nêu ra, tôi hiếm khi khuyên mọi người có ít hơn.


1
Đây là một giải pháp thực tế, nhưng mặc dù chúng tôi đang nhanh chóng nhận được nhiều lõi hơn, số lượng lõi trên mỗi nút NUMA khá trì trệ ở khoảng 4. Vậy trên nút lõi 1000 giả thuyết, chúng tôi sẽ chạy 250 quy trình MPI chứ? (Điều này sẽ rất tuyệt, nhưng tôi nghi ngờ.)
Jed Brown

Tôi không đồng ý rằng số lượng lõi trên mỗi NUMA bị trì trệ. Sandy Bridge E5 có 8. Magny Cours có 12. Tôi đã có một nút West 4.0.3-EX với 10. Interlagos (ORNL Titan) có 20. Góc Hiệp sĩ sẽ có hơn 50. Tôi đoán rằng các lõi trên NUMA đang giữ bắt kịp với Định luật Moore, ít nhiều.
Bill Barth

Magny Cours và Interlagos có hai điểm chết ở các vùng NUMA khác nhau, do đó 6 và 8 lõi cho mỗi vùng NUMA. Tua lại đến năm 2006, nơi hai ổ cắm Clovertown lõi tứ sẽ chia sẻ cùng một giao diện (chipset Blackford) và tôi không thấy số lượng lõi trên mỗi vùng NUMA đang tăng nhanh như vậy. Blue Gene / Q mở rộng tầm nhìn bộ nhớ phẳng này thêm một chút và có lẽ Góc của Knight sẽ tiến thêm một bước (mặc dù đó là một thiết bị khác, vì vậy có lẽ chúng ta nên so sánh với GPU thay vào đó, nơi chúng ta có 15 (Fermi) hoặc bây giờ là 8 ( Kepler) SM xem bộ nhớ phẳng).
Jed Brown

Cuộc gọi tốt trên các chip AMD. Tôi đã quên mất. Tuy nhiên, tôi nghĩ rằng bạn sẽ thấy sự tăng trưởng liên tục trong lĩnh vực này trong một thời gian.
Bill Barth

6

Câu trả lời này là để đáp lại hai quan niệm sai lầm liên quan đến C ++ trong câu hỏi.

  1. "Điều tương tự cũng áp dụng cho toán tử mới C ++, đòi hỏi phải khởi tạo phân bổ mới (bao gồm cả POD)"
  2. "Toán tử C ++ mới chỉ mất một tham số"

Nó không phải là một câu trả lời trực tiếp cho các vấn đề đa lõi mà bạn đề cập. Chỉ cần trả lời các bình luận phân loại các lập trình viên C ++ là những người nhiệt thành C ++ để danh tiếng được duy trì;).

Đến điểm 1. C ++ "mới" hoặc phân bổ ngăn xếp không khăng khăng khởi tạo các đối tượng mới, cho dù là POD hay không. Hàm tạo mặc định của lớp, như được xác định bởi người dùng, có trách nhiệm đó. Mã đầu tiên bên dưới hiển thị rác được in cho dù lớp đó có phải là POD hay không.

Đến điểm 2. C ++ cho phép nạp chồng "mới" với nhiều đối số. Mã thứ hai dưới đây cho thấy một trường hợp như vậy để phân bổ các đối tượng đơn lẻ. Nó sẽ đưa ra một ý tưởng và có lẽ hữu ích cho tình huống bạn có. toán tử mới [] cũng có thể được sửa đổi một cách thích hợp.

// Mã cho điểm 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';
}

Trình biên dịch 11.1 của Intel cho thấy đầu ra này (tất nhiên là bộ nhớ chưa được khởi tạo được chỉ bởi "a").

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

// Mã cho điểm 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ảm ơn đã sửa chữa. Dường như C ++ không biến chứng thêm không có mặt so với C, ngoại trừ mảng phi POD như std::complexđược khởi tạo một cách rõ ràng.
Jed Brown

1
@JedBrown: Lý do số 6 để tránh sử dụng std::complex?
Jack Poulson

1

Trong thỏa thuận.II, chúng tôi đã có cơ sở hạ tầng phần mềm để song song lắp ráp trên mỗi ô trên nhiều lõi bằng cách sử dụng Khối xây dựng luồng (về bản chất, bạn có một tác vụ cho mỗi ô và cần lên lịch các tác vụ này lên các bộ xử lý có sẵn - đó không phải là cách thực hiện nhưng đó là ý tưởng chung). Vấn đề là để tích hợp cục bộ, bạn cần một số đối tượng tạm thời (cào) và bạn cần cung cấp ít nhất là có nhiều tác vụ có thể chạy song song. Chúng tôi thấy tốc độ kém, có lẽ là do khi một tác vụ được đưa lên bộ xử lý, nó sẽ lấy một trong các đối tượng cào thường có trong bộ đệm của một số lõi khác. Chúng tôi có hai câu hỏi:

(i) Đây thực sự là lý do? Khi chúng tôi chạy chương trình theo bộ nhớ cache, tôi thấy rằng về cơ bản tôi đang sử dụng cùng một số lượng hướng dẫn như khi chạy chương trình trên một luồng, nhưng tổng thời gian chạy tích lũy trên tất cả các luồng lớn hơn nhiều so với luồng đơn. Có thực sự bởi vì tôi liên tục lỗi bộ nhớ cache?

(ii) Làm cách nào tôi có thể biết được mình đang ở đâu, từng đối tượng cào ở đâu và đối tượng cào nào tôi cần phải truy cập vào đối tượng nóng trong bộ đệm của lõi hiện tại của tôi?

Cuối cùng, chúng tôi đã không tìm thấy câu trả lời cho một trong những giải pháp này và sau khi một vài công trình quyết định rằng chúng tôi thiếu các công cụ để điều tra và giải quyết những vấn đề này. Tôi biết cách ít nhất về nguyên tắc giải quyết vấn đề (ii) (cụ thể là sử dụng các đối tượng luồng cục bộ, giả sử rằng các luồng vẫn được ghim vào lõi bộ xử lý - một phỏng đoán khác không tầm thường để kiểm tra), nhưng tôi không có công cụ nào để kiểm tra vấn đề (Tôi).

Vì vậy, từ quan điểm của chúng tôi, việc đối phó với NUMA vẫn là một câu hỏi chưa được giải quyết.


Bạn nên liên kết các chủ đề của mình với các ổ cắm để bạn không phải tự hỏi liệu bộ xử lý có được ghim hay không. Linux thích di chuyển mọi thứ xung quanh.
Bill Barth

Ngoài ra, lấy mẫu getcpu () hoặc calendar_getcpu () (tùy thuộc vào libc và kernel của bạn và whatnot) sẽ cho phép bạn xác định nơi các luồng đang chạy trên Linux.
Bill Barth

Có, và tôi nghĩ rằng các khối xây dựng luồng mà chúng ta sử dụng để lên lịch làm việc trên các luồng xử lý các luồng cho các bộ xử lý. Đây là lý do tại sao chúng tôi đã cố gắng làm việc với lưu trữ luồng cục bộ. Nhưng tôi vẫn gặp khó khăn khi đưa ra giải pháp cho vấn đề của mình (i).
Wolfgang Bangerth

1

Ngoài hwloc, có một vài công cụ có thể báo cáo về môi trường bộ nhớ của cụm HPC và có thể được sử dụng để đặt nhiều cấu hình NUMA khác nhau.

Tôi muốn giới thiệu LIKWID như một công cụ như vậy vì nó tránh một cách tiếp cận dựa trên mã cho phép bạn lấy ví dụ để ghim một quy trình vào lõi. Cách tiếp cận này của công cụ để giải quyết cấu hình bộ nhớ cụ thể của máy sẽ giúp đảm bảo tính di động của mã của bạn trên các cụm.

Bạn có thể tìm thấy một bản trình bày ngắn phác thảo nó từ ISC'13 " LIKWID - Công cụ hiệu suất nhẹ " và các tác giả đã xuất bản một bài báo về Arxiv " Thực hành tốt nhất cho kỹ thuật hiệu suất được hỗ trợ bởi HPM trên bộ xử lý đa lõi hiện đại ". Bài viết này mô tả một cách tiếp cận để giải thích dữ liệu từ các bộ đếm phần cứng để phát triển mã biểu diễn cụ thể cho cấu trúc liên kết kiến ​​trúc và bộ nhớ của máy.


LIKWID rất hữu ích, nhưng câu hỏi liên quan đến cách viết các thư viện nhạy cảm với số / bộ nhớ có thể có được một cách đáng tin cậy và tự kiểm toán địa phương dự kiến ​​trên một loạt các môi trường thực thi, sơ đồ luồng, quản lý tài nguyên MPI và cài đặt mối quan hệ, sử dụng với các thư viện khác, v.v.
Jed Brown
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.