ポータブルマルチコア/ NUMAメモリ割り当て/初期化のベストプラクティス


17

メモリ帯域幅が制限された計算が共有メモリ環境(OpenMP、Pthreads、またはTBBを介したスレッドなど)で実行される場合、各スレッドがほとんどのメモリに物理的にアクセスするようにメモリを物理メモリに正しく分散させる方法のジレンマがあります「ローカル」メモリバス。インターフェイスは移植性がありませんが、ほとんどのオペレーティングシステムにはスレッドアフィニティを設定する方法があります(たとえばpthread_setaffinity_np()、多くのPOSIXシステム、sched_setaffinity()Linux、SetThreadAffinityMask()Windows)。メモリ階層を決定するためのhwlocなどのライブラリもありますが、残念ながら、ほとんどのオペレーティングシステムにはNUMAメモリポリシーを設定する方法がまだ用意されていません。Linuxは顕著な例外であり、libnumaがありますアプリケーションがページの粒度でメモリポリシーとページ移行を操作できるようにします(2004年以降メインラインにあるため、広く利用可能です)。他のオペレーティングシステムでは、ユーザーが暗黙の「ファーストタッチ」ポリシーに従うことを期待しています。

「ファーストタッチ」ポリシーを使用すると、呼び出し側は、新しく割り当てられたメモリに最初に書き込むときに使用する予定の親和性でスレッドを作成および配布する必要があります。(非常に少数のシステムがあるように構成されているmalloc()、それはちょうど彼らが実際に障害が発生しているときに別のスレッドによって、おそらく、それらを見つけることを約束、実際にページを検索します。)これは、使用してその割り当てを暗示しcalloc()たり、すぐに使用して割り当てが後にメモリを初期化するmemset()ことがフォルトする傾向があるので、有害です割り当てスレッドを実行しているコアのメモリバス上のすべてのメモリ。複数のスレッドからメモリにアクセスすると、最悪のメモリ帯域幅になります。同じことは、new多くの新しい割り当ての初期化を要求するC ++ 演算子にも当てはまります(例:std::complex)。この環境に関するいくつかの観察:

  • 割り当ては「スレッド集合」にすることができますが、異なるスレッドモデルを使用してクライアントと対話しなければならないライブラリ(望ましくはそれぞれ独自のスレッドプール)には望ましくない割り当てがスレッドモデルに混在するようになりました。
  • RAIIは慣用的なC ++の重要な部分であると考えられていますが、NUMA環境でのメモリパフォーマンスには積極的に有害であるようです。配置newは、malloc()から割り当てられたメモリまたはからのルーチンで使用できますlibnumaが、これにより割り当てプロセスが変更されます(これは必要だと思います)。
  • 編集:演算子に関する私の以前の声明newは間違っていた、それは複数の引数をサポートすることができます、チェタンの応答を参照してください。ライブラリーまたはSTLコンテナーが指定されたアフィニティーを使用することへの懸念がまだあると思います。複数のフィールドがパックされている場合があり、たとえば、std::vector正しいコンテキストマネージャをアクティブにして再割り当てすることを保証するのは不便です。
  • 各スレッドは独自のプライベートメモリを割り当ててフォールトできますが、隣接する領域へのインデックス作成はより複雑になります。(スパース行列ベクトル積の検討行列とベクトルの行パーティションと、の所有されていない部分インデックス作成、xは、より複雑なデータ構造が必要Xは仮想メモリに連続していない)をyAバツバツバツ

NUMAの割り当て/初期化の解決策は慣用的と見なされますか?他の重要な落とし穴を省きましたか?

(C ++の例がその言語に重点を置くことを意味するわけではありませんが、C ++ 言語は、Cのような言語にはないメモリ管理に関するいくつかの決定をエンコードします。物事が異なります。)

回答:


7

私が好む傾向があるこの問題の解決策の1つは、スレッドと(MPI)タスクを効果的にメモリコントローラーレベルで分解することです。つまり、CPUソケットまたはメモリコントローラーごとに1つのタスクを作成し、各タスクの下にスレッドを作成することで、コードからNUMAの側面を削除します。そのようにすると、どのスレッドが実際に割り当てまたは初期化の作業を行うかに関係なく、ファーストタッチまたは利用可能なAPIのいずれかを介して、すべてのメモリをそのソケット/コントローラーに安全にバインドできるはずです。少なくともMPIでは、ソケット間でのメッセージの受け渡しは通常非常に最適化されています。常にこれよりも多くのMPIタスクを実行できますが、提起する問題のために、これより少ないタスクを推奨することはめったにありません。


1
これは実用的なソリューションですが、急速にコアを増やしているにもかかわらず、NUMAノードあたりのコアの数は4前後でかなり停滞しています。したがって、仮想1000コアノードでは、250 MPIプロセスを実行しますか。(これは素晴らしいことですが、私は懐疑的です。)
ジェドブラウン

NUMAあたりのコアの数が停滞していることに同意しません。Sandy Bridge E5には8があります。MagnyCoursには12があります。10のWestmere-EXノードがあります。Interlagos(ORNL Titan)には20があります。KnightsCornerには50を超えるでしょう。ムーアの法則とほぼ同じペース。
ビル・バルト

Magny CoursとInterlagosには、異なるNUMAリージョンに2つのダイがあり、NUMAリージョンごとに6コアと8コアがあります。クアッドコアクローバータウンの2つのソケットがメモリへの同じインターフェース(Blackfordチップセット)を共有する2006年に巻き戻します。NUMAリージョンごとのコア数がそれほど急速に増加しているようには思えません。Blue Gene / Qはこのメモリのフラットビューをもう少し拡張し、Knight's Cornerが別のステップを踏むかもしれません(ただし、別のデバイスなので、代わりにGPU(15(Fermi)または現在8(ケプラー)フラットメモリを表示するSM)。
ジェドブラウン

AMDチップの呼び出し。私は忘れていました。それでも、この分野ではしばらくの間、継続的な成長が見られると思います。
ビル・バルト

6

この回答は、質問における2つのC ++関連の誤解に対する回答です。

  1. 「新しい割り当て(PODを含む)の初期化を要求するC ++ new演算子にも同じことが当てはまります。」
  2. 「C ++演算子newは1つのパラメーターのみを取ります」

あなたが言及したマルチコアの問題に対する直接的な答えではありません。評判が維持されるように、C ++プログラマーをC ++熱狂者として分類するコメントに返信するだけです;)。

ポイント1. C ++の「新規」またはスタック割り当ては、PODであるかどうかにかかわらず、新しいオブジェクトの初期化を要求しません。ユーザーが定義したクラスのデフォルトコンストラクターがその責任を負います。次の最初のコードは、クラスがPODであるかどうかにかかわらず、印刷されたジャンクを示しています。

ポイント2。C++では、複数の引数で「新規」をオーバーロードできます。以下の2番目のコードは、単一オブジェクトを割り当てるこのようなケースを示しています。それはアイデアを与え、おそらくあなたの状況に役立つはずです。演算子new []も適切に変更できます。

//ポイント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';
}

Intelの11.1コンパイラは、この出力を示しています(もちろん、「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 ++のような非PODの配列を除いてCに比べて存在する追加の合併症、ないものと思わstd::complexている明示的に初期化します。
ジェドブラウン

1
@JedBrown:使用を避ける理由番号6 std::complex
ジャックポールソン

1

deal.IIには、スレッドビルディングブロックを使用して各セルのアセンブリを複数のコアに並列化するソフトウェアインフラストラクチャがあります(本質的に、セルごとに1つのタスクがあり、これらのタスクを利用可能なプロセッサにスケジュールする必要があります-それはそうではありません実装されていますが、それは一般的なアイデアです)。問題は、ローカル統合には多数の一時(スクラッチ)オブジェクトが必要であり、少なくとも並行して実行できるタスクと同じ数のタスクを提供する必要があることです。おそらく、タスクがプロセッサに投入されると、通常は他のコアのキャッシュにあるスクラッチオブジェクトの1つを取得するため、スピードアップは不十分です。2つの質問がありました。

(i)これが本当に理由ですか?cachegrindでプログラムを実行すると、プログラムを単一スレッドで実行する場合と基本的に同じ数の命令を使用していることがわかりますが、すべてのスレッドで累積された合計実行時間は、シングルスレッドの実行時間よりもはるかに長くなります。それは本当にキャッシュをフォールトし続けているからでしょうか?

(ii)現在のコアのキャッシュでホットなオブジェクトにアクセスするために、現在の場所、各スクラッチオブジェクトの場所、およびどのスクラッチオブジェクトを取得する必要があるかを確認するにはどうすればよいですか?

最終的には、これらのソリューションのいずれに対する答えも見つかりませんでした。いくつかの作業の結果、これらの問題を調査して解決するツールが不足していると判断しました。少なくとも原則として問題を解決する方法は知っています(ii)(つまり、スレッドローカルオブジェクトを使用して、スレッドがプロセッサコアにピン留めされたままであると仮定します-テストするのは簡単ではない別の推測)、しかし、私は問題をテストするツールがありません(私)。

したがって、私たちの観点から見ると、NUMAの取り扱いは未解決の問題です。


スレッドをソケットにバインドして、プロセッサが固定されているかどうかを気にする必要がないようにする必要があります。Linuxはいろいろなものを動かすのが好きです。
ビル・バルト

また、getcpu()またはsched_getcpu()をサンプリングすると(libcとカーネルなどに応じて)、スレッドがLinuxのどこで実行されているかを判別できるはずです。
ビル・バルト

はい、そして、スレッドへの作業をスケジュールするために使用するスレッディングビルディングブロックは、スレッドをプロセッサに固定します。これが、スレッドローカルストレージを使用しようとした理由です。しかし、私の問題の解決策を思いつくのはまだ難しい(i)。
ウォルフガングバンガース

1

hwloc以外にも、HPCクラスターのメモリ環境についてレポートできるツールがいくつかあり、さまざまなNUMA構成を設定するために使用できます。

このようなツールの1つとしてLIKWIDをお勧めします。コードベースのアプローチを回避できるため、たとえばプロセスをコアに固定できます。マシン固有のメモリ構成に対処するツールのこのアプローチは、クラスター間でのコードの移植性を確保するのに役立ちます。

ISC'13「LIKWID-Lightweight Performance Tools」から概要を説明した短いプレゼンテーションを見つけることができ、著者はArxiv「最新のマルチコアプロセッサでのHPM支援パフォーマンスエンジニアリングのベストプラクティス」に関する論文を公開しています。このペーパーでは、ハードウェアカウンタからのデータを解釈して、マシンのアーキテクチャとメモリトポロジに固有のパフォーマンスコードを開発する方法について説明します。


LIKWIDは便利ですが、問題は、さまざまな実行環境、スレッドスキーム、MPIリソース管理、親和性設定で期待される局所性を確実に取得および自己監査できる数値/メモリ依存ライブラリの作成方法に関するものでした。他のライブラリなど
ジェドブラウン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.