NUMAとメモリアクセス局所性
マルチソケットで性能が伸びない理由が腑に落ちます。ノード間アクセスの非対称コスト、メモリポリシー、自動NUMAバランシングを設計判断の軸で押さえられます。
- 1.NUMAではCPUとメモリがノードに分かれ、自ノードより遠いノードへのアクセスは帯域が低く遅延が大きい非対称コストを持つ。
- 2.メモリポリシー(local/bind/interleave)でページの配置先ノードを制御し、局所性を取るか帯域を均すかを使い分ける。
- 3.自動NUMAバランシングはアクセス傾向を観測してページとタスクを同じノードへ寄せるが、移動コストとのトレードオフがある。
なぜアクセスコストが一様でなくなるのか
UMA(Uniform Memory Access)では、すべての CPU が単一のメモリバスを共有し、どのアドレスへも同じコストで届きます。素直な設計ですが、コア数が増えると共有バスとメモリコントローラがボトルネックになり、コアを足しても帯域が頭打ちになります。
NUMA(Non-Uniform Memory Access)はこの限界を、メモリコントローラを分散させて解きます。CPU コア群とその近傍のメモリ(DRAM)を1つのノードにまとめ、ノード同士を高速インターコネクト(Intel UPI、AMD Infinity Fabric など)でつなぎます。各コアは自ノードのメモリには内蔵コントローラ経由で直結しますが、他ノードのメモリへはインターコネクトを1ホップ以上越えて到達します。
[Node 0] [Node 1]
CPU0 CPU1 ── 内蔵MC ── DRAM DRAM ── 内蔵MC ── CPU2 CPU3
| |
+============ インターコネクト ================+
(ノード間アクセスはここを通る=余分な遅延と帯域制約)
結果としてアクセスコストは非対称になります。同じ命令・同じデータ量でも、対象ページがどのノードにあるかで遅延と実効帯域が変わる。これが「メモリアクセスが Non-Uniform」の意味です。
ファームウェアは ACPI の SLIT(System Locality Information Table)でノード間の相対距離を公開します。慣例上、自ノードは 10、リモートはおおむね2倍前後(例 20〜21)で表現されます。Linux からは numactl --hardware で各ノードのメモリ量と距離行列を確認でき、スケジューラやアロケータはこの距離を配置判断の入力に使います。
ローカルとリモートの差は何に効くか
リモートアクセスのペナルティは「遅延」と「帯域」の2軸で現れます。遅延はインターコネクトのホップ分が上乗せされ、典型的にローカルの 1.5〜2 倍程度になります。帯域はインターコネクトの容量に律速され、さらに競合が効きます。全コアが特定の1ノードに集中アクセスすると、そのノードのメモリコントローラとインターコネクトが飽和し、ローカル相当の理論帯域は出せません。
| 観点 | ローカルアクセス | リモートアクセス |
|---|---|---|
| 経路 | 内蔵メモリコントローラへ直結 | インターコネクトを1ホップ以上経由 |
| 相対遅延 | 基準(SLITで10) | おおむね1.5〜2倍(SLITで21など) |
| 実効帯域 | ノードのMC帯域をフルに使える | インターコネクト容量と競合に律速 |
| スケール特性 | ノードを足すほど総帯域が増える | 片寄ると総帯域が増えない |
設計上の含意は明快です。スレッドと、そのスレッドが触るデータを同じノードへ寄せれば、総帯域はノード数に比例して伸びます。逆に配置が無頓着だと、コアを増やしてもインターコネクト越しの競合で性能が飽和する——これが NUMA を意識しない並列プログラムでよく起きる「スケールしない」現象の正体です。
メモリポリシー:ページをどのノードに置くか
物理ページがどのノードから割り当てられるかは、仮想記憶のページフォルト時に確定します。Linux はこの割当先を選ぶ規則をメモリポリシーとして制御でき、numactl や mbind(2) / set_mempolicy(2) で指定します。代表的な3つを比べます。
| ポリシー | 割当先の決め方 | 向く状況 |
|---|---|---|
| local(既定) | フォルトを起こしたコアのノードから割り当て | スレッドとデータの局所性を最大化したい |
| bind | 指定したノード集合に限定して割り当て | 配置を厳密に固定したい・隔離したい |
| interleave | ページ単位でノードを順番に分散 | 全ノードへ均等アクセスし帯域を稼ぎたい |
既定の local(first-touch とも呼ぶ) が重要な性質を持ちます。割当先は「ページを確保したスレッド」ではなく「そのページに最初に触れたスレッドが走っていたノード」で決まる、という点です。
malloc(巨大な配列) # まだ物理ページは割り当てられない(仮想だけ予約)
for 各要素: 配列[i] = 0 # ここで初めてフォルト → 触れたコアのノードに配置
つまり「確保したスレッド」と「実際に使うスレッド」が別ノードだと、データは確保側ノードに固まり、使用側からは全部リモートアクセスになります。並列コードでよく使う定石が first-touch 初期化——本番で各データを読むのと同じスレッド分担で初期化フォルトを起こし、データを使用ノードへ正しく分散させる手法です。
大きな配列をメインスレッド1つで memset してから OpenMP 等で並列処理すると、全ページが初期化を行ったノードに集中配置されます。以降そのノードのメモリコントローラとインターコネクトが集中アクセスで飽和し、他ノードのコアは延々リモートアクセスを強いられます。確保ではなく first-touch のタイミングと担当スレッドこそが配置を決める、という非直感的な点が事故の温床です。
interleave は逆の発想です。1つの大きな領域を意図的に全ノードへ分散させ、どのコアから見ても「半分ローカル・半分リモート」に均します。最良ケースの局所性は捨てる代わりに、特定ノードへの集中による飽和を避け、アクセスが全域に散る用途(巨大な共有ハッシュ表など)で総帯域を引き上げます。局所性を取るか、片寄り回避を取るかの設計判断です。
アロケータとの関係
物理ページの実体を配るのはカーネルのメモリアロケータで、buddy system の free list はノードごと(ゾーンごと)に独立して管理されます。メモリポリシーは「どのノードのフリーリストから先に取るか」の優先順を与え、アロケータはそれに従って候補ノードを巡回します。
希望ノードに空きが無いときの挙動も設計判断に直結します。bind で厳格に縛れば、そのノードが逼迫すると回収(reclaim)やスワップが起きてでもノードを守ろうとし、最悪 OOM に至ります。一方 local(preferred 相当の緩い扱い)なら、希望ノードが満杯のとき近い別ノードへフォールバックします。局所性の厳格さと、メモリ逼迫時の頑健さはトレードオフの関係にあります。
vm.zone_reclaim_mode を有効にすると、ローカルノードが詰まったとき他ノードへフォールバックする前にローカルの回収を試みます。局所性は守られますが、ページキャッシュが頻繁に追い出され、ファイル I/O 主体のワークロード(DB やファイルサーバー)ではかえって性能が落ちることがあります。多くのディストリビューションが既定で無効にしているのはこのためです。
自動NUMAバランシングとスケジューラ連携
手動配置は強力ですが、すべてのアプリを書き換えるのは非現実的です。そこで Linux は 自動NUMAバランシング(Automatic NUMA Balancing) を備えます。狙いは「タスクと、そのタスクが実際に触るページを、同じノードへ自律的に寄せる」ことです。
仕組みは2段構えです。まずカーネルが定期的に一部のページのページテーブルエントリを意図的にアクセス不可(PROT_NONE 相当)に印付けします。タスクがそのページに触れると軽量なフォルト(NUMA hinting fault)が発生し、カーネルは「どのタスクが・どのノードのページに触れたか」を統計として記録します。これは TLB やテーブルウォークの仕組み(MMU/TLB の内部)を流用した、アクセス傾向のサンプリングです。
1. 定期スキャン: 一部ページのPTEを「アクセス不可」に印付け
2. タスクが触る → NUMA hinting fault 発生
3. 統計更新: そのタスクは「どのノードのメモリ」を多く触るか集計
4. 判断:
- ページをタスクのいるノードへ移す(ページマイグレーション) または
- タスクを、よく触るページのあるノードへ移す(タスク移動)
集まった統計をもとにカーネルは2方向の是正を選びます。ページをタスク側へ動かすか、タスクをページ側へ動かすか。後者は CPU スケジューラとの連携で、タスクが多くアクセスするノードを「希望ノード」としてスケジューラに伝え、可能ならそのノードのコアへ寄せます。負荷分散(ロードバランシング)が複数ノードへ均そうとする力と、局所性が1ノードへ寄せようとする力のせめぎ合いを、CFS/後継スケジューラが重み付けして調停します。
ページマイグレーションは物理コピーと全CPUのTLB無効化(シュートダウン)を伴う実コストです。アクセスパターンが定まらないと、寄せた直後に逆へ動かす振動(ping-pong)が起き、本来の計算より移動で時間を食う事態になります。レイテンシ重視で配置を自分で固定したい場合は、numactl --cpunodebind --membind で CPU とメモリを同一ノードにピン留めし、自動バランシング(numa_balancing)を切るのが定石です。
NUMA で押さえるべきは「local=first-touch で配置が決まる」「interleave は局所性を捨てて片寄りを防ぐ」「bind は厳格だが逼迫時に脆い」「自動バランシングは観測ベースで寄せるが移動コストと振動のリスクがある」の4点です。確保時ではなく最初に触れたときに配置が決まる点が、頻出かつ誤りやすい急所です。
まとめ
- NUMA はメモリコントローラを分散してUMAの帯域限界を超えるが、代償としてノード間アクセスは遅延・帯域とも非対称になる。
- 既定の local(first-touch) では、確保時ではなく最初に触れたノードへページが配置される。並列コードは first-touch 初期化で使用ノードへ正しく分散させる。
- interleave は局所性を捨てて全ノードへ均し集中飽和を防ぐ、bind は厳格だがメモリ逼迫時に脆い——目的に応じて選ぶ。
- 自動NUMAバランシングはアクセスを観測してページとタスクを同ノードへ寄せるが、移動コストと振動があるため、レイテンシ重視ではピン留めして自動化を切る判断も有効。
OS Article
NUMAとメモリアクセス局所性を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
NUMA
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 5
導入後に効く点
メモリポリシー(local/bind/interleave)でページの配置先ノードを制御し、局所性を取るか帯域を均すかを使い分ける。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 5
判断チェックリスト
- 自社の用途が「NUMA / メモリ」に近いか確認する。
- 強みである「NUMAではCPUとメモリがノードに分かれ、自ノードより遠いノードへのアクセスは帯域が低く遅延が大きい非対称コストを持つ。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。