HPC Pack 2012 R2 Update 3 以降では、GPU リソースを管理および監視し、GPU リソースを完全に利用するようにコンピューティング ノード上の GPGPU ジョブをスケジュールできます。
この記事の内容:
展開
サポートされている GPU カードがインストールされたコンピューティング ノードを手動でデプロイするために必要な追加の手順はありません。 既存の任意の方法に従って、HPC Pack クラスターにコンピューティング ノードを追加できます。
注:
現在、HPC Pack では、CUDA と互換性のある製品 (NVidia Tesla シリーズなど) のみがサポートされています。 サポートされている製品の詳細な一覧については、Nvidia サイトの を参照してください。
管理と監視
GPU ノード グループ
GPU コンピューティング ノードが正常にデプロイされると、新しい既定のノード グループ (GPUNodes) が自動的に作成されます。 サポートされている GPU リソースで認識されるすべてのノードがこのグループに追加されます。
また、クラスター内のコンピューティング リソースを整理する方法に基づいて、GPU ノードを含む新しいカスタマイズされたノード グループを作成することもできます。 これは、他のノード タイプでカスタム グループを作成する場合と似ています。
ノードのプロパティ
HPC クラスター マネージャーで GPU コンピューティング ノードを選択すると、ノード プロパティの詳細の [GPU 情報] タブに基本情報が表示されます。 基本的な GPU 情報には、次のものが含まれます。
GPU カード PIC バス ID
GPU モジュール名
GPU の合計メモリ
GPU SM クロック (MHz でのマルチプロセッサ クロックのストリーミング)
GPU ヒート マップ
次の GPU メトリックが、クラスター ノードのヒート マップ ビューに追加されます。
GPU 時間 (%)
GPU の電力使用量 (ワット)
GPU メモリ使用量 (%)
GPU メモリ使用量 (MB)
GPU ファン速度 (%)
GPU 温度 (°C)
GPU SM クロック (MHz)
ヒート マップの独自のビューをカスタマイズして、他の既存のヒート マップ操作と同じ方法で GPU 使用率を監視できます。
1 つのコンピューティング ノードに複数の GPU がある場合は、複数のメトリック値が表示されます。
GPU ジョブのスケジュール設定
HPC Pack の CUDA コーディングのベスト プラクティス (この記事の後半を参照) に従うことで、クラスター内の GPU リソースを完全に利用するように HPC Pack によって GPU ジョブをスケジュールできます。 これは、HPC クラスター マネージャー、HPC PowerShell、または HPC Pack ジョブ スケジューラ API で行うことができます。
HPC クラスター マネージャー
HPC クラスター マネージャーで GPU ジョブを送信するには
ジョブ管理で、[新しいジョブ 。クリック ジョブの詳細 で、[ジョブ リソースの] の下にある [GPU 選択します。 リソース選択で、GPU ノードを含むノード グループを選択します。 それ以外の場合、GPU リソースでスケジュールされたジョブは失敗します。
ジョブを実行するノードを指定することもできます。 これらのノードも GPU ノードである必要があります。
残りの設定を完了し、ジョブを送信します。
HPC PowerShell
HPC PowerShell を使用して GPU ジョブを送信するには、次のサンプル コマンドのように NumGpus パラメーターを指定します。
$job = New-HpcJob -NumGpus 1
Add-HpcTask -Job $job -CommandLine "<CommandLine>"
Submit-HpcJob -Job $job
HPC Pack ジョブ スケジューラ API
ジョブ スケジューラ API を使用して GPU ジョブを送信する場合は、次のコード スニペットに示すように、UnitType
を JobUnitType.Gpu
に設定します。
Scheduler sc = new Scheduler();
sc.Connect(<ClusterName>);
ISchedulerJob job = sc.CreateJob();
sc.AddJob(job);
job.UnitType = JobUnitType.Gpu;
ISchedulerTask task = job.CreateTask();
task.CommandLine = "<CommandLine>";
job.AddTask(task);
sc.SubmitJob(job, <username>, <password>);
ジョブ テンプレート
GPU ジョブのスケジュール設定を簡略化するには、GPU 関連の設定を事前に準備するジョブ テンプレートを作成します。
GPU 設定でジョブ テンプレートを作成するには
構成で、適切な初期設定で新しいジョブ テンプレートを作成します。
ジョブ テンプレート エディター の [ノード グループ] で、GPU ノードを含む 1 つ以上のノード グループ必須として指定します。 ジョブ テンプレートのプロパティで、新しい ユニットの種類 設定を追加します。 GPU 既定値 し、有効な値として選択します。 これにより、GPU ジョブを GPU に基づいて簡単にスケジュールできます。
CUDA コード サンプル
HPC Pack (HPC Pack 2012 R2 Update 3 以降) で提供される GPU スケジューリングは簡単です。 HPC Pack ジョブ スケジューラは、使用可能な GPU リソースを持つコンピューティング ノードにのみジョブとタスクを割り当てます。
ジョブがノードに割り当てられると、最初に、このマシンで適切な GPU ユニットが使用されるようにします。 HPC Pack 環境変数CCP_GPUIDSを使用して、この情報を直接取得します。 コード スニペットを次に示します。
/* Host main routine */
int main(void)
{
// get the available free GPU ID and use it in this thread.
cudaSetDevice(atoi(getenv("CCP_GPUIDS")));
// other CUDA operations
}
// once job finishes, GPU will be freed up and recognized by job scheduler as available to assign to other job/tasks.
呼び出し getenv("CCP_GPUIDS")
の戻り値は、使用可能な GPU のインデックスを含む文字列です。 最も一般的なケースでは、各タスクには 1 つの GPU が必要であり、戻り値は 0、1 などの 1 つのインデックス番号を含む文字列になります。
次の例では、詳細を示します。
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <vector>
#include <sstream>
#include <iostream>
#include <ctime>
#include <algorithm>
__global__ void spinKernel(int count)
{
for (int i = 0; i < count; i++)
{
for (int j = 0; j < 1024; j++)
{
int hello = 0;
int world = 1;
int helloWorld = hello + world;
}
}
}
bool sortFunc(std::pair<int, std::string> *p1, std::pair<int, std::string> *p2)
{
return p1->second.compare(p2->second) < 0;
}
void getSortedGpuMap(std::vector<std::pair<int, std::string> *> &map)
{
int count = 0;
cudaGetDeviceCount(&count);
for (int i = 0; i < count; i++)
{
char buffer[64];
cudaDeviceGetPCIBusId(buffer, 64, i);
// Build a mapping between cuda Device Id and Pci Bus Id
map.push_back(new std::pair<int, std::string>(i, std::string(buffer)));
}
// Sort by Pci Bus Id
std::sort(map.begin(), map.end(), sortFunc);
}
int main()
{
// In case different processes get different cuda Device Ids, sort them by Pci Bus Id
// Not necessary if the devices always give the same Ids in a machine
std::vector<std::pair<int, std::string> *> deviceMap;
getSortedGpuMap(deviceMap);
cudaError_t cudaStatus = cudaSuccess;
std::vector<int> myGpuIds;
// Get the process-wide environment variable set by HPC
std::istringstream iss(getenv("CCP_GPUIDS"));
clock_t startTime = clock();
clock_t endTime;
do
{
std::string idStr;
iss >> idStr;
// Drop the ending chars
if (idStr.length() == 0)
{
break;
}
int gpuId = atoi(idStr.c_str());
std::cout << "GPU ID parsed: " << gpuId << std::endl;
// Set the device ID
cudaStatus = cudaSetDevice(deviceMap[gpuId]->first);
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaSetDevice failed! Current GPU ID: %d", deviceMap[gpuId]->first);
return 1;
}
std::cout << "GPU with HPC ID " << gpuId << " has been set" << std::endl;
std::cout << "Cuda Device ID: " << deviceMap[gpuId]->first << std::endl << "Pci Bus Id: " << deviceMap[gpuId]->second << std::endl;
// Launch a kernel to spin GPU, async by default
spinKernel <<<1024, 1024>>> (1024);
endTime = clock();
double elapsed = (double)(endTime - startTime) / CLOCKS_PER_SEC;
startTime = endTime;
printf("Spin kernel launched after %.6f seconds\n\n", elapsed);
// Record the device IDs for further usage
myGpuIds.push_back(deviceMap[gpuId]->first);
} while (iss);
for (int i = 0; i < deviceMap.size(); i++)
{
// Clean up the map since device IDs have been saved
delete deviceMap[i];
}
std::cout << "Waiting for all the kernels finish..." << std::endl << std::endl;
for (std::vector<int>::iterator it = myGpuIds.begin(); it != myGpuIds.end(); it++)
{
// Set the device ID
cudaStatus = cudaSetDevice(*it);
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaSetDevice failed! Current GPU ID: %d", *it);
return 1;
}
cudaDeviceSynchronize();
}
endTime = clock();
double elapsed = (double)(endTime - startTime) / CLOCKS_PER_SEC;
startTime = endTime;
printf("Spin kernels finished after %.6f seconds\n\n", elapsed);
for (std::vector<int>::iterator it = myGpuIds.begin(); it != myGpuIds.end(); it++)
{
// Set the device ID
cudaStatus = cudaSetDevice(*it);
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaSetDevice failed! Current GPU ID: %d", *it);
return 1;
}
std::cout << "GPU with Device ID " << *it << " has been set" << std::endl;
// Do cleanup
cudaStatus = cudaDeviceReset();
if (cudaStatus != cudaSuccess) {
fprintf(stderr, "cudaDeviceReset failed! Current GPU ID: %d", *it);
return 1;
}
std::cout << "Reset device done" << std::endl << std::endl;
}
return 0;
}