ExpEtherという仮想化技術により、ホスト(CPU)と複数のデバイス(GPU)を10Gbps Ethernetで接続するシステムである。
ExpEtherではPCIeのパケットをEthernetのフレームにカプセル化するということを行っている。 そのため、このシステムのユーザからはホストのマザーボードに直接GPUが挿さっているように見える。
GPU-BOXはこれのプロトタイプで、8個のExpEther NICと3000Wの電源が付いた箱のこと。 最大8台のGPUを搭載することができる。
このGPU-BOXは、2013年度卒の野村先輩(通称のむさん)から脈々と実機評価@NECで使われていた。 しかし、2015年2月から市販品のExpEther I/O拡張ユニット (N8000-1005) が使われるようになる。 基本的には入れ物が変わっただけで、ExpEther NICに変化は無いため、性能に差はない。
このシステムの売りは主に拡張性にある。 ExpEther I/O拡張ユニットをEthernet switchにつなぐだけでGPUの増設が簡単に行えるからである。 「この拡張性が既存のデータセンタに光をもたらす」とか云々言って論文のIntroductionが始まるのである。
CPU | Intel Xeon E5-1650 @ 3.20GHz |
Host Memory | 16GB |
OS | CentOS 6.3 |
CUDA | Toolkit 5.5 |
GPU | NVIDIA Tesla K20 ×4 |
ExpEther board | NEC N8007-104 |
I/O expansion box | NEC N8000-1005 ×2 |
Switch | Mellanox SX1012 ×2 |
Network | 10Gb Ethernet ×2 |
ここでいうNormal systemとは、1つのマザーボードのPCIeスロットに複数のGPUが付いているシステムのことを指す。
基本的にNormal systemでできることはExpress systemでもできる。 つまり、Normal systemのコードがそのままExpress systemでも利用でき、正しく動くということ。
ただし、利用できるからといって実際に使い物になるとは限らない。 PCIeの帯域幅と10Gbps Ethernetの帯域幅に大きな差があるためである。 PCIeの世代にもよるが、だいたい"10Gbps Ethernetの帯域幅 = 1/5 x PCIeの帯域幅"くらいの差がある。
このことを考慮していないコードが性能的に使い物にならないということは容易に想像がつくだろう。
とりあえずここで言いたいことは、実機評価@NECでは油断するな、ということ。
Express system@NECのホストのPCIeスロットには、 ExpEther NICの他にディスプレイ出力用のNVIDIA製GPUが1つ挿さっている。 上述したが、ユーザからは外付けGPUもホストマシンに内蔵しているように見えてしまうため、 ユーザには外付けGPUsと内臓GPUが見えることになる。
試しにnvidia-smiを実行すると以下のようになる。 この環境では、Tesla C2050がホストに直挿しされていて、他のTesla K20はすべて外付けされている。
$ nvidia-smi
#ref(): File not found: "nvidia-smi.txt" at page "Multi-GPU system with ExpEther"
よく見ると、GPU名の左横になにやらIDのようなものが振ってあることがわかる。 これを見て、「今回はTesla C2050は使わないから、GPU 0以外を使わないようにしよう!」と考えてしまいがちだが、これは罠である。
GPU IDはdeviceQueryで確認しなければならない。
$ /usr/local/cuda/samples/1_Utilities/deviceQuery/deviceQuery
#ref(): File not found: "deviceQuery.txt" at page "Multi-GPU system with ExpEther"
deviceQueryにより、実際は次のようにGPU IDが振られていることが確認できる。
どうやらNVIDIAは、シングルGPU実行時では自動的に最新(もしくは最高性能)のGPUを優先的に使うようにしているらしい。
以上により、外付けのGPUは0, 2, 3, 4番であるとわかった。
"どのGPUが外付けなの?"の環境を仮定して、サンプルを提示する。
基本的に以下のような関数をプログラムの冒頭で1回だけ行えば良い。 以下で行われるCUDA runtime関数は意外と重たいので注意。
static int id_table[] = { 0, 2, 3, 4 }; // 使いたいGPU IDを列挙する void InitializeDevices(int num_gpus) { for (int i = 0; i < num_gpus; ++i) { for (int j = 0; j < num_gpus; ++j) { int gpu_id = id_table[i]; int peer_id = id_table[j]; if (gpu_id == peer_id) continue; cudaSetDevice(gpu_id); int can_access_peer = 0; cudaDeviceCanAccessPeer(&can_access_peer, gpu_id, peer_id); if (can_access_peer == 0) continue; printf("Enabling peer access to GPU %d from GPU %d\n", peer_id, gpu_id); cudaDeviceEnablePeerAccess(peer_id, 0); } } }
$ ifconfig eth16 down $ ifconfig eth16 192.168.0.3 $ ifconfig eth16 up
written by mits(2015-02-14)