このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
アルバイトの西田です。
IntelのC for Metal(以前はC for Mediaという名前だったもの、略してC4MまたはCM)を、7月中はインターンとして、8月からはアルバイトとしてその使い方や性能を調べていました。
この記事はその成果をまとめたものです。
※MacのGPU言語のMetalとは関係ありません。
C for Metal言語の言語仕様については/documents/compiler/html/cmlangspec/cmlangspec.htmlに書いてあります。
大まかなプログラムはホストプログラム(vectoradd.cpp)に書いて並列処理を行う場所をカーネルプログラム(vectoradd_genx.cpp)に書きます。
まずはプラットフォームがmedia-driverに対応しているかの確認をしてください。
※ここに書かれているプラットフォームでないとCforMetalが動かないので気をつけてください
C for Metal公式サイトよりファイルをダウンロードする
以降、ここを<package_root>とします。
$ cd drivers $ sudo ./install.sh
drivers/media-driverからmedia-driverパッケージのインストールをします。
$ sudo dpkg -i intel-media-2.xxx.deb
CMアプリケーションの構築や実行に必要なlibva,libdrmライブラリもメディアドライバーとともにインストールされます。
drivers/IGCからIGCパッケージをインストールします。
$ sudo dpkg -i IGC-xxx.deb
$ export LIBVA_DRIVER_NAME=iHD $ export LIBVA_DRIVERS_PATH=/usr/lib/x86_64-linux-gnu/dri $ export CM_ROOT=<package_root>
$ make $ ./hw_x64.vectoradd
このサンプルプログラムは2つのvectorを読み込み、その和を出力するプログラムです。
<package_root>/compiler/bin/cmc vectoradd_genx.cpp -isystem <package_root>/compiler/include -o vectoradd_genx.isa
上記のコマンドを実行することで、カーネルプログラム(-genx.cpp)をCM Compilerに通してビルドし、ISAファイルを作成します。
g++ vectoradd.cpp -w -g -I<package_root>/runtime/include -msse4.1 -D__LINUX__ -DLINUX -O0 -std=gnu++11 -fPIC -c -rdynamic -ffloat-store -o vectoradd.hw_x64.og++ vectoradd.hw_x64.o -lva -ldl -fPIC -rdynamic <package_root>/runtime/lib/x64/igfxcmrt64.so -o hw_x64.vectoradd
上記のコマンドによってホストプログラムをビルドし、実行ファイルであるhw_x64.vectoraddが生成されます。
./hw_x64.vectoradd
そして、それを上記のコマンドによってその実行ファイルを実行することで、並列処理が実行されます。
_GENX_MAIN_ void add(SurfaceIndex ibuf1, SurfaceIndex ibuf2, SurfaceIndex obuf) { static const int N = 64; const int x = get_thread_origin_x() * N; vector<unsigned char, N> m1, m2, out; read(ibuf1, x, m1); read(ibuf2, x, m2); out = m1 + m2; write(obuf, x, out) }
addという命令セットを作成します。addという命令セットはibuf1とibuf2を読み込んでそれぞれの要素における合計をobufに返す命令セットです。
N | 1スレッドで処理を行う要素数。 |
get_thread_origin() | 今行っている命令セットが何個目のスレッドかを取得する関数です。 |
x | このスレッドで処理する先頭のインデックスであるxが得られます。 |
C for Metal言語ではvector<type, size>
で長さsizeで要素の型がtypeであるvectorが生成されます。 read関数とwrite関数はCforMetalで標準に用意されている関数です。
read
void read(SurfaceIndex IND, int offset, vector_ref v);
read (DWALIGNED(ind), ...) // INDがDWordで構成されている場合 read(ind, ...) // INDがOwordで構成されている場合
write
void write(SurfaceIndex IND, int offset, const vector v);
IND | 出力するSurfaceIndex。 |
offset | readのoffsetと変わりません。 |
v | INDに出力するvector。 |
CmDevice *device = nullptr; UINT version = 0; cm_result_check(CreateCmDevice(device, version));
CreateCmDevice
INT CreateCmDevice (CmDevice *& device, UINT & version, VADisplay vaDisplay = nullptr);
Linuxの中に、ハードウェアモードのCM Deviceを作成します。CmDeviceをイチから、またはinput vaDisplay interface上に作成します。イチから作成する場合、internal vaDisplay interfaceが作成され、その上にCmDeviceが作成されます。
device | 作成するCmDeviceのポインタ。 |
version | CM APIのバージョン。 |
vaDisplay | nullptrではなかった場合、vaInitializeから得られたVADIsplayを参照します。 |
FILE* isa_file = fopen("vectoradd_genx.isa", "rb"); if(isa_file == NULL) { std::exit(1); } fseek (isa_file, 0, SEEK_END); const int isa_size = ftell (isa_file); void *isa_code = malloc(isa_size); rewind(isa_file); fread(isa_code, sizeof(void), isa_size, isa_file); fclose(isa_file); CmProgram *program = nullptr; cm_result_check(device->LoadProgram(isa_code,isa_size, program));
LoadProgram
virtual INT CmDevice::LoadProgram (void *pCommonISACode,const UINT size, CmProgram *&pProgram,const char * options = nullptr)
pCommonISACode | ISAファイルの中身のデータ。 |
size | ISAファイルの大きさ。 |
pProgram | 作成するCmProgramのポインタ。 |
CmKernel *kernel = nullptr;cm_result_check(device->CreateKernel(program, "add", kernel));
vectoradd_genx.cppの中にあるaddという関数を実行するカーネルを作成します。
CreateKernel
virtual INT CmDevice::CreateKernel(CmProgram * pProgram, const char * kernelName, CmKernel *& pKernel, const char * options=nullptr)
pProgram | Kernelを作成するCmProgram。 |
kernelName | カーネルの名前。 |
pKernel | 作成するカーネルのポインタ。 |
const unsigned int thread_width = N / 64; CmThreadSpace *thread_space = nullptr; cm_result_check(device->CreateThreadSpace(thread_width, 1, thread_space));
スレッドスペース(Thread space)
CreateThreadSpace
virtual INT CmDevice::CreateThreadSpace(UINT width, UINT height, CmThreadSpace *& pTS)
width | スレッドスペースの幅。 |
height | スレッドスペースの高さ。 |
pTS | 作成するCmThreadSpaceのポインタ。 |
CmTask * task = nullptr;cm_result_check(device->CreateTask(task));
CmTaskにはCmKernelが格納され、カーネルを実行キューに入れるために使われます。
CreateTask
virtual INT CmDevice::CreateTask(CmTask *& pTask)
pTask | 作成するCmTaskのポインタ。 |
cm_result_check(task->AddKernel(kernel));
AddKernel
virtual INT CmTask::AddKernel(CmKernel *pKernel)
CmTaskにCmKernelを格納する。
pKernel | 格納するカーネル。 |
CmBuffer *m1_surface = nullptr; SurfaceIndex *m1_surface_idx = nullptr; cm_result_check(device->CreateBuffer(N, m1_surface)); cm_result_check(m1_surface->WriteSurface((unsigned char*)m1, nullptr)); cm_result_check(m1_surface->GetIndex(m1_surface_idx)); CmBuffer *m2_surface = nullptr; SurfaceIndex *m2_surface_idx = nullptr; cm_result_check(device->CreateBuffer(N, m2_surface)); cm_result_check(m2_surface->WriteSurface((unsigned char*)m2, nullptr)); cm_result_check(m2_surface->GetIndex(m2_surface_idx)); CmBuffer *output_surface;SurfaceIndex *output_surface_idx = nullptr; cm_result_check(device->CreateBuffer(N, output_surface)); cm_result_check(output_surface->GetIndex(output_surface_idx));
CreateBuffer
virtual INT CmDevice::CreateBuffer(UINT size, CmBuffer *& pSurface)
CmBufferを作成する関数です。
size | 作成するCmBufferの配列の大きさ。 |
pSurface | 作成するCmBufferのポインタ。 |
WriteSurface
virtual int32_t CmBuffer::WriteSurface(const unsigned char * sysMem, CmEvent * event, uint64_t sysMemSize=0xFFFFFFFFFFFFFFFFULL)
CPUを使ってシステムメモリの内容をCmBufferにコピーする関数です。この関数はブロッキング関数でコピーが終わるまで値を返しません。Bufferを書く操作はeventがCM_STATUS_FINISHEDになるまで行われません。WriteSurfaceのeventは普通はNULLです。もしsysMemSizeが与えられて、それがCmBufferの大きさよりも小さければ、sysMemSize分だけコピーされます。
sysMem | CmBufferへコピーするシステムメモリのポインタ。 |
event | 同期のために使うイベントのポインタ。 |
sysMemSize | コピーするbytesの数。 |
GetIndex
virtual int32_t CmBuffer::GetIndex(SurfaceIndex *& index)
indexにCmBufferオブジェクトのSurfaceIndexオブジェクトを返す関数です。
index | 参照するSurfaceIndexのポインタ。 |
cm_result_check(kernel->SetKernelArg(0, sizeof(SurfaceIndex), m1_surface_idx)); cm_result_check(kernel->SetKernelArg(1, sizeof(SurfaceIndex), m2_surface_idx)); cm_result_check(kernel->SetKernelArg(2, sizeof(SurfaceIndex), output_surface_idx));
kernelの引数にm1,m2,outをそれぞれ設定します。
SetKernelArg
virtual INT CmKernel::SetKernelArg(UINT index, size_t size, const void * pValue)
カーネルの引数を設定する関数です。
index | カーネル側の何番目の引数を設定するかの値。 |
size | 引数として設定する変数の型の大きさ。 |
pValue | SurfaceIndexのポインタ。 |
CmQueue *cmd_queue = nullptr;cm_result_check(device->CreateQueue(cmd_queue));
CreateQueue
virtual INT CmDevice::CreateQueue(CmQueue *& pQueue)
CmQueueを作成する関数です。
pQueue | 作成するCmQueueのポインタ。 |
CmEvent *sync_event = nullptr; for (int i = 0; i < num_iters; ++i) { UINT64 time_in_ns = 0; cm_result_check(cmd_queue->Enqueue(task, sync_event, thread_space)); cm_result_check(sync_event->WaitForTaskFinished(time_out)); cm_result_check(sync_event->GetExecutionTime(time_in_ns)); kernel_time_in_ns += time_in_ns; }
CM eventはEnqueueを実行するために作成され、ジョブの状態が保存されます。
Enqueue
virtual INT CmQueue::Enqueue(CmTask * pTask, CmEvent *& pEvent, const CmThreadSpace * pTS = nullptr)
Enqueueはノンブロッキングな呼び出し(GPUがジョブの実行を終えるのを待たずに値を返してしまうもの)です。runtimeはハードウェアの状態を参照し、ハードウェアがジョブでいっぱい出なかったらジョブをEnqueueし、そうでなかったら待ってからEnqueueを実行します。そういう特性をもっているのでWaitForTaskFinished関数を使ってEnqueueが終わるまで待機させることが多いと思います。
pTask | CmQueueにプッシュするTaskのポインタ。 |
pTS | 参照するCmThreadSpaceのポインタ。 |
WaitForTaskFinished
virtual INT CmEvent::WaitForTaskFinished(DWORD dwTimeOutMs=2*1000)
CmEventが終わるまで待機する関数です。ここではEnqueueの実行が完全に終了するまで待機します。
dwTimeOutMs | タイムアウトする時間。 |
GetExecutionTime
virtual INT CmEvent::GetExecutionTime(UINT64 & time)
GPUでジョブが実行された時間を返す関数です。
time | 実行時間を保存する変数。 |
cm_result_check(output_surface->ReadSurface((unsigned char*)out, sync_event));
output surfaceが読み込まれるときにCM eventが使われているのはCM_STATUS_FINISHEDになるまで読み込まれないようにするためです。
ReadSurface
virtual int32_t CmBuffer::ReadSurface(unsigned char * sysMem, CmEvent * event, uint64_t sysMemSize=0xFFFFFFFFFFFFFFFFULL)
CPUを使ってCmBufferの内容をシステムメモリにコピーする関数です。この関数はブロッキング関数です。Bufferを書く操作はeventがCM_STATUS_FINISHEDになるまで行われません。もしsysMemSizeが与えられて、それがCmBufferの大きさよりも小さければ、sysMemSize分だけコピーされます。
sysMem | CmBufferへコピーするシステムメモリのポインタ。 |
event | 同期のために使うイベントのポインタ。 |
sysMemSize | コピーする大きさ。 |
cm_result_check(device->DestroyTask(task)); cm_result_check(device->DestroyThreadSpace(thread_space)); cm_result_check(cmd_queue->DestroyEvent(sync_event)); cm_result_check(::DestroyCmDevice(device));
DestroyTask
virtual INT CmDevice::DestroyTask(CmTask *& pTask)
CmTaskを破壊する関数です。CmTaskはCmDeviceが破壊されるときに破壊されますが、ここでは単独で破壊しています。
pTask | 破壊するCmTaskのポインタ。 |
DestroyThreadSpace
virtual INT CmDevice::DestroyThreadSpace(CmThreadSpace *& pTS)
CmThreadSpaceを破壊する関数です。CmThreadSpaceもまたCmDeviceが破壊されるときに破壊されますが、ここでは単独で破壊してます。
pTS | 破壊するスレッドスペースのポインタ。 |
DestroyEvent
virtual INT CmQueue::DestroyEvent(CmEvent *& pEvent)
CmEventを破壊する関数です。CmEventは明示的に破壊しなければなりません。
pEvent | 破壊するCmEventのポインタ。 |
DestroyCmDevice
INT DestroyCmDevice (CmDevice *& device)
CmDeviceを破壊する関数です。CmDeviceが破壊されると、そのCmDeviceで初期化されたsurface、kernel、task、thread space、queueも破壊されます。
device | 破壊するCmDeviceのポインタ。 |
CPU | Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz |
OS | Ubuntu 18.04.3 LTS |
メモリ | 16GiB |
vectorの大きさ | 256 | 1024 | 4096 | 8192 | 16384 | 24576 | 32768 | 40960 | 49152 | 57344 | 65536 |
CPU[msec] | 0.0005 | 0.0027 | 0.0091 | 0.0181 | 0.0366 | 0.0544 | 0.0715 | 0.0890 | 0.1089 | 0.1237 | 0.1429 |
C for Metal[msec] | 0.063 | 0.063 | 0.063 | 0.062 | 0.064 | 0.065 | 0.065 | 0.066 | 0.067 | 0.067 | 0.068 |
C for Metal(kernel)[msec] | 0.0062 | 0.0069 | 0.0064 | 0.0068 | 0.0079 | 0.0085 | 0.0091 | 0.0096 | 0.0104 | 0.0110 | 0.0118 |
1000回実行したときの中央値を表示しています。
CPU | CPU操作だけでvectoraddを行った際の実行時間。 |
C for Metal | Enqueueをしてから計算を終えるまでの実行時間。 |
C for Metal(kernel) | 上の実行時間の中でkernelが計算した時間。 |
CPUとC for Metalを比較することでC for Metalの速さが比較できます。kernelのみの時間とCforMetal全体の時間のギャップは同期処理や排他制御によって生じる時間であると考えられます。
今回はCforMetalの概要とCPUとの速度比較を行いました。
配列が小さいうちは、カーネルを呼び出す時間などのオーバーヘッドなどを考慮するとCPUのほうが実行時間が短くなりますが、配列の大きさが65536になるとCPUよりもCforMetalのほうが高速になっています。
今後の課題は、OpenCLやCUDAとの速度比較、更にはもう少し複雑なプログラムでの速度比較を行っていきたいです。
コンピュータビジョンセミナーvol.2 開催のお知らせ - ニュース一覧 - 株式会社フィックスターズ in Realizing Self-Driving Cars with General-Purpose Processors 日本語版
[…] バージョンアップに伴い、オンラインセミナーを開催します。 本セミナーでは、...
【Docker】NVIDIA SDK Managerでエラー無く環境構築する【Jetson】 | マサキノート in NVIDIA SDK Manager on Dockerで快適なJetsonライフ
[…] 参考:https://proc-cpuinfo.fixstars.com/2019/06/nvidia-sdk-manager-on-docker/ […]...
Windowsカーネルドライバを自作してWinDbgで解析してみる① - かえるのほんだな in Windowsデバイスドライバの基本動作を確認する (1)
[…] 参考:Windowsデバイスドライバの基本動作を確認する (1) - Fixstars Tech Blog /proc/cpuinfo ...
2021年版G検定チートシート | エビワークス in ニューラルネットの共通フォーマット対決! NNEF vs ONNX
[…] ONNX(オニキス):Open Neural Network Exchange formatフレームワーク間のモデル変換ツー...
YOSHIFUJI Naoki in CUDAデバイスメモリもスマートポインタで管理したい
ありがとうございます。別に型にこだわる必要がないので、ユニバーサル参照を受けるよ...