問題の概要:GPU間通信のエラーとパフォーマンス低下
マルチGPU環境(例:RTX 3090, A100, H100を2枚以上搭載)で深層学習の分散学習や大規模なシミュレーションを実行する際、以下のような問題に遭遇することがあります。
- エラーメッセージ:
cudaErrorPeerAccessUnsupported: peer access is not supported between these two devices - GPU間でデータを転送する際の帯域幅が、PCIeバスの理論値(例:PCIe 4.0 x16で約32GB/s)よりも大幅に低い
- NVLink対応GPUを搭載しているにもかかわらず、その高速インターフェースが活用されている実感がない
これらの問題は、GPU間の直接通信(Peer-to-Peer, P2P)が正しく設定・活用されていない場合に発生します。特に、NVLinkという専用の高速相互接続技術が利用可能な環境では、その設定がパフォーマンスに決定的な影響を与えます。
原因の解説:P2P通信の制約とNVLinkの条件
CUDAにおけるGPU間のPeer-to-Peer通信は、すべてのハードウェア構成で自動的に有効になるわけではありません。主な原因は以下の3つです。
1. ハードウェアおよびドライバの制限
P2P通信は、同じ物理マシン内の特定のGPU組み合わせでのみサポートされます。異なるアーキテクチャ(例:Tesla V100とGeForce RTX 4090)や、一部の古いGPU間ではサポートされていない場合があります。また、ドライバやCUDA Toolkitのバージョンが古いと、新しいハードウェアの機能を十分に活用できません。
2. PCIeトポロジーの問題
マザーボードのPCIeレーン割り当てや、CPUのPCIeコントローラ(NUMAノード)をまたがるGPU間では、P2P通信が禁止されることがあります。例えば、2つのGPUが異なるCPUソケットに接続されている場合、直接通信できない構成が一般的です。
3. NVLinkの未接続・未認識
NVLinkは物理的なブリッジ(NVLink Bridge)でGPUを接続する必要があります。このブリッジが正しく装着されていない、または対応していないブリッジ(世代違いなど)を使用している場合、NVLinkは有効化されず、通信は低速なPCIeバスにフォールバックします。
解決方法:ステップバイステップでの確認と設定
ステップ1: システムトポロジーとP2P対応の確認
まず、現在のシステムでどのGPU間でP2P通信が可能かを確認します。CUDAサンプルコードまたはnvidia-smiコマンドを使用します。
# nvidia-smiでトポロジーを確認
nvidia-smi topo -m
# 出力例(P2P対応状況が表示される):
# GPU0 GPU1 GPU2 GPU3
# GPU0 X PHB SOC SOC
# GPU1 PHB X SOC SOC
# GPU2 SOC SOC X PHB
# GPU3 SOC SOC PHB X
#
# PHB: P2Pは可能だが、パフォーマンスはPCIeホストブリッジに制限
# SOC: P2Pは可能で、同じCPUソケット上にあるため最適
# NODE: P2P不可(異なるNUMAノード)
# NV: NVLink接続(nはリンク数)
また、以下のような簡単なCUDAプログラムでP2Pアクセス可否をテストできます。
// p2p_test.cu (コンパイル: nvcc p2p_test.cu -o p2p_test)
#include <stdio.h>
#include <cuda_runtime.h>
int main() {
int num_devices;
cudaGetDeviceCount(&num_devices);
printf("Number of GPUs: %dn", num_devices);
for (int i = 0; i < num_devices; i++) {
for (int j = 0; j < num_devices; j++) {
if (i == j) continue;
int can_access;
cudaDeviceCanAccessPeer(&can_access, i, j);
printf("GPU%d -> GPU%d : P2P Access %sn",
i, j, can_access ? "OK" : "NO");
}
}
return 0;
}
ステップ2: NVLink状態の確認と物理接続の見直し
nvidia-smiコマンドでNVLinkの状態を詳細に確認します。
# NVLinkの帯域幅とエラー情報を表示
nvidia-smi nvlink -c
nvidia-smi nvlink -s
# 各GPUのNVLink情報を表示(GPUインデックスを指定)
nvidia-smi -i 0 --query-gpu=nvlink_bandwidth.total --format=csv
出力にNVLinkリンクが表示されない、または帯域幅が0の場合は、以下の点を確認してください。
- GPUがNVLinkに対応しているか(コンシューマ向けGeForceの多くは非対応)
- 正しい世代のNVLinkブリッジが確実に装着されているか
- マザーボードのスロット間隔がブリッジの物理的形状と合っているか
ステップ3: CUDAプログラム内でのP2P有効化
ハードウェア的にP2Pが可能でも、ソフトウェア側で明示的に有効化する必要があります。以下のコードは、すべての可能なGPUペア間でP2Pアクセスを有効にする典型的な方法です。
// enable_p2p.cu
#include <cuda_runtime.h>
#include <stdio.h>
int enableP2P(int num_gpus) {
for (int i = 0; i < num_gpus; i++) {
cudaSetDevice(i);
for (int j = 0; j < num_gpus; j++) {
if (i == j) continue;
int can_access;
cudaError_t err = cudaDeviceCanAccessPeer(&can_access, i, j);
if (err != cudaSuccess) {
printf("Failed to query P2P access between GPU%d and GPU%d: %sn",
i, j, cudaGetErrorString(err));
continue;
}
if (can_access) {
err = cudaDeviceEnablePeerAccess(j, 0); // フラグは0
if (err == cudaSuccess) {
printf("Enabled P2P access GPU%d -> GPU%dn", i, j);
} else if (err == cudaErrorPeerAccessAlreadyEnabled) {
printf("P2P access already enabled GPU%d -> GPU%dn", i, j);
} else {
printf("Failed to enable P2P access GPU%d -> GPU%d: %sn",
i, j, cudaGetErrorString(err));
}
} else {
printf("P2P access not supported GPU%d -> GPU%dn", i, j);
}
}
}
return 0;
}
この設定後、cudaMemcpyPeerやcudaMemcpyPeerAsync関数を使用して、GPU間で直接データを転送できるようになります。
ステップ4: フレームワークでの設定(PyTorch例)
深層学習フレームワークを使用する場合、多くの場合は内部でP2Pを自動的に扱いますが、明示的に環境変数を設定することで最適化できる場合があります。
# PyTorchでNVLinkを最大限活用するための環境変数設定例
export NCCL_DEBUG=INFO
export NCCL_P2P_DISABLE=0 # P2Pを明示的に有効化(デフォルト)
export NCCL_P2P_LEVEL=NVL # NVLink経由の通信を優先
export NCCL_IB_DISABLE=1 # InfiniBandを使用しない場合(クラスタ環境でない場合)
# PyTorchスクリプト内での確認
import torch
print(f"Available GPUs: {torch.cuda.device_count()}")
for i in range(torch.cuda.device_count()):
for j in range(torch.cuda.device_count()):
if i != j:
can_access = torch.cuda.can_device_access_peer(i, j)
print(f"GPU{i} can access GPU{j}: {can_access}")
コード例・コマンド例
ベンチマーク:P2P転送速度の測定
以下のコードで、PCIe経由とNVLink経由(設定されている場合)の実際の転送速度を比較できます。
// p2p_bandwidth.cu (簡略版)
#include <cuda_runtime.h>
#include <stdio.h>
#include <chrono>
void benchmarkP2P(int src_gpu, int dst_gpu, size_t size) {
cudaSetDevice(src_gpu);
float* src_data;
cudaMalloc(&src_data, size);
cudaSetDevice(dst_gpu);
float* dst_data;
cudaMalloc(&dst_data, size);
// ウォームアップ
cudaMemcpyPeer(dst_data, dst_gpu, src_data, src_gpu, size);
auto start = std::chrono::high_resolution_clock::now();
int iterations = 100;
for (int i = 0; i < iterations; i++) {
cudaMemcpyPeer(dst_data, dst_gpu, src_data, src_gpu, size);
}
cudaDeviceSynchronize();
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed = end - start;
double bandwidth = (size * iterations * 2) / (elapsed.count() * 1e9); // GB/s
printf("GPU%d -> GPU%d Bandwidth: %.2f GB/sn",
src_gpu, dst_gpu, bandwidth);
cudaFree(src_data);
cudaFree(dst_data);
}
トラブルシューティングコマンド集
# 基本的なシステム情報確認
nvidia-smi
nvidia-smi topo -m
# NVLinkの詳細状態(A100, H100等)
nvidia-smi nvlink -i 0 -c # GPU0のNVLinkカウンタを表示
nvidia-smi nvlink -i 0 -g # GPU0のNVLinkエラーカウンタをリセット
# CUDAドライバとランタイムのバージョン確認
cat /usr/local/cuda/version.txt
nvcc --version
# PCIeリンク速度確認(Gen3 x16なら8.0GT/s、Gen4なら16.0GT/s)
lspci -vv | grep -i nvidia -A 20 | grep LnkSta
まとめ・補足情報
CUDA Peer-to-Peer通信とNVLinkの活用は、マルチGPUシステムのパフォーマンスを最大限引き出すための重要な技術です。要点をまとめます。
- 事前確認が重要:
nvidia-smi topo -mでハードウェア的なP2P対応を必ず確認しましょう。これにより、ソフトウェア設定以前の問題を早期に発見できます。 - NVLinkは物理接続が命: 対応GPUと正しいブリッジの装着が必須です。特に、サーバー向けGPU(A100, H100)とワークステーション向けGPU(RTX 6000 Ada)では、NVLinkの形態(NVLink Bridge vs NVLink Connector)が異なる場合があるので注意が必要です。
- ソフトウェアでの明示的有効化: ハードウェアが対応していても、CUDAでは
cudaDeviceEnablePeerAccess()を呼び出して初めてP2P通信が可能になります。フレームワークを使用する場合も、環境変数などで最適化を図りましょう。 - パフォーマンスベンチマークの実施: 理論値と実際の転送速度には乖離が生じることがあります。実際のワークロードに近い条件でベンチマークを実施し、システムが期待通りに動作しているかを検証してください。
最後に、一般的な注意点として、P2Pアクセスを有効にするとGPUメモリがロックされ、他のプロセスからの使用が制限される場合があります。また、すべてのアプリケーションがP2P通信の恩恵を受けるわけではなく、GPU間で頻繁に大規模データを交換するワークロード(大規模モデルの並列訓練、分子動力学シミュレーションなど)で最も効果を発揮します。自身のユースケースに合わせて、適切な設定を行うことが重要です。