GCPのGPUインスタンスでレンダリングを高速化

2017/04/10
このエントリーをはてなブックマークに追加

はじめに

カブクの甘いもの担当兼コーヒーマシン清掃担当の高橋憲一です。
今回は最近GCPで使用可能になったGPUインスタンスを活用して3Dモデルのレンダリングを高速化したことについて書きます。

カブクのレンダリングエンジン

Kabuku Connect、Kabuku MMS、Rinkak等、カブクが提供しているサービスで使うための3Dモデルのプレビュー画像はサーバーサイドで動作する自社開発エンジンでレンダリングしています。グラフィクスAPIとしてOpenGLを使用しており、これまではOpenGL互換のライブラリであるMesaを使ってオフスクリーンレンダリングを行っていました。それにより、GPUを搭載していないマシンでもCPUの処理のみでレンダリングすることができます。ただしCPUのみでのレンダリングはやはり時間がかかるため、クラウドのインスタンスでのGPU活用は2年程前のエンジン運用投入当初より切望していたことでした。
llvmpipe版というLLVMのランタイムコード生成でシェーダーやラスタライズを行うバージョンのMesaを使用することで、CPUのみの環境でも少しでも速くレンダリングできるようにはしています)

このレンダリング機能は、創業初期はblenderをpythonで制御して実現していたのですが、3Dモデルの解析エンジンを開発した際に、モデルデータをパースして頂点座標をロードできているのだから後はOpenGLの呼び出しをすればレンダリングエンジンにできるのではないかと考えたことから開発を始めたものです。OpenGLを使ったのは私が慣れ親しんでいたAPIだということもありますが、OpenGLを使っていれば将来クラウド環境でもGPUが使えるようになったときにコードはほぼそのままで高速化が可能になるということを開発当初から念頭に置いていました。

GCP

カブクではGoogle Cloud Platform (GCP)を使用してサービスを提供しており、レンダリングエンジンはGoogle Compute Engine (GCE)で動いています。そのGCEでGPUを搭載したインスタンスを使うことができるようになったのを受けて、GPUのパワーを活かしてレンダリングできるように対応しました。

GPU対応するにあたりエンジンの改修で必要だったのは、EGLによるOpenGLのコンテキスト生成、オフスクリーンでレンダリングするためのフレームバッファオブジェクトの生成の2つでした。

LinuxでのOpenGL

OpenGLのAPIを呼び出すにはまずコンテキストの生成が必要です。OpenGL自体はクロスプラットフォームなAPIですが、各OS毎にそのコンテキストの生成と画面描画システム(ウィンドウシステム)とのバインディングを担う層があります。通常、LinuxでOpenGLを使おうとするとGLXとXlibというX Windowに関連する層のAPIを使用することになります。X serverを動作させたり、さらにディスプレイがつながれていないヘッドレスなサーバーで実行する(オフスクリーンでのレンダリング)となると設定に難儀するなと当初は考えあぐねておりました。そこで今時Xを使わずに済む、もっとシンプルな方法はないものかと調べたところEGLを使う方法が見つかりました。

NVIDIAのデベロッパーブログEGL Eye: OpenGL Visualization without an X Serverに「X Serverを使わずにOpenGLビジュアライゼーションを行う」ための情報が詳しく載っており、「ついこの間まではOpenGLのコンテキストを管理するにはX serverを走らせておく必要があったが、今はEGLを使うことができる」という内容になっています。

ここでEGLと聞いて身に覚えがあるのは、数年前AndroidでJavaではなくNDKを使ってC++でOpenGL ESを使う実装をした時のことです。そこから来る認識はEGLはOpenGLに使うものでなく、OpenGL ESとセットで使う組み込み用途…というものでした。しかし、NVIDIAのドライバーのバージョン355以降ではOpenGLでも使用可能になっています。

EGLの取扱自体はNVIDIAの記事にも “creating an OpenGL context with EGL is not rocket science!” (EGLでOpenGLのコンテキストを生成することはロケット科学のような難しいことではない!)とあるようにとてもシンプルで、実装に時間はかかりませんでした。そしてOpenGLのAPIを呼ぶところや頂点シェーダーおよびフラグメントシェーダーはMesaで使用していたものをそのまま変更することなく使えますし、手元でのテスト用にMac OS X用に実装していたフレームバッファオブジェクト生成のコードもプラットフォーム非依存のOpenGLのAPIのために共通で使うことができました。

Mesaを使用する場合と異なるところはEGLに関する部分と、フレームバッファオブジェクトの生成部分で以下のようになります。

EGLでのコンテキスト生成(NVIDIAのブログにあったコードを参考にエラーチェックを追加したもの)


int width = 1024;
int height = 1024;
EGLDisplay display;
EGLContext context;

// 1. EGLの初期化
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
    fprintf(stderr, "failed to eglGetDisplay\n");
    return false;
}
EGLint major, minor;
EGLBoolean eglStatus = eglInitialize(display, &major, &minor);
if (eglStatus == EGL_FALSE) {
    fprintf(stderr, "failed to eglInitialize\n");
    return false;
}
fprintf(stderr, "EGL Version %d . %d\n", major, minor);

// 2. RGBやdepthバッファのビット数等の設定を選択
static const EGLint configAttribs[] = {
      EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
      EGL_BLUE_SIZE, 8,
      EGL_GREEN_SIZE, 8,
      EGL_RED_SIZE, 8,
      EGL_DEPTH_SIZE, 8,
      EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
      EGL_NONE
};
EGLint numConfigs;
EGLConfig eglCfg;
eglStatus = eglChooseConfig(display, configAttribs, &eglCfg, 1, &numConfigs);
if (eglStatus == EGL_FALSE) {
    fprintf(stderr, "failed to eglChooseConfig\n");
    return false;
}
// 3. サーフェスの生成
const EGLint pbufferAttribs[] = {
    EGL_WIDTH, width,
    EGL_HEIGHT, height,
    EGL_NONE
};
EGLSurface surface = eglCreatePbufferSurface(display, eglCfg, pbufferAttribs);
if (surface == EGL_NO_SURFACE) {
    fprintf(stderr, "failed to eglCreatePbufferSurface\n");
    return false;
}
// 4. API(OpenGL)のバインド
eglStatus = eglBindAPI(EGL_OPENGL_API);
if (eglStatus == EGL_FALSE) {
    fprintf(stderr, "failed to eglBindAPI\n");
    return false;
}
// 5. コンテキストを生成し、カレントにする
context = eglCreateContext(display, eglCfg, EGL_NO_CONTEXT, NULL);
if (context == EGL_NO_CONTEXT) {
    fprintf(stderr, "failed to eglCreateContext\n");
    return false;
}
eglStatus = eglMakeCurrent(display, surface, surface, context);
if (eglStatus == EGL_FALSE) {
    fprintf(stderr, "failed to eglMakeCurrent\n");
    return false;
}

フレームバッファオブジェクト生成


GLuint frameBuffer;
GLuint renderBuffer;

glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);

// RGBA用のバッファ
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                 GL_RENDERBUFFER, renderBuffer);

// depthバッファ
GLuint depthRenderbuffer;
glGenRenderbuffers(1, &depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                      GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);

status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
    fprintf(stderr, "failed to initialize frame buffer object\n");
}

あとは1フレームあたりに必要なOpenGLのAPIを一通り実行したら以下のようにするとフレームバッファの内容をあらかじめ確保した領域に読み込むことができるので、その領域を画像ファイルとして保存します。


uchar *imageBuffer = new uchar[width * height * 4];
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, imageBuffer);

余談ですが、実はMesaでオフスクリーンレンダリングする際は上記でやっていたことに相当するコードは以下の部分だけです。


int width = 1024;
int height = 1024;
uchar *imageBuffer = new uchar[width * height * 4];
OSMesaContext context = OSMesaCreateContextExt(GL_RGBA, 24, 0, 0, 0);
OSMesaMakeCurrent(context, imageBuffer, GL_UNSIGNED_BYTE, width, height);

後は1フレームあたりに必要なOpenGLのAPIを一通り実行したらimageBufferが示す領域にはレンダリングされた画像が入っているので、その内容を適切な画像ファイルとして保存するだけです。(glReadPixlesも必要ありません)

いざ実行してみると…

まずはローカルのLinuxマシン(Tensorflowを使ったDeep LearningにおけるGPU性能調査でも登場したGTX980Tiを積んだ開発機)で高速レンダリングできることを確認し、意気揚々とクラウドのインスタンスで実行してみると…ローカルのマシンで得られたような期待する性能が出ません。遅くなっている箇所を調査したところ、EGLコンテキスト生成に異常に時間がかかっていることが分かり、手元にあるマシンで実行した際は0.01秒で済んでいたものが2.60秒もかかるのです。これでは折角のGPUによる高速化の意味も半減してしまいますので、現象を再現するためのテストプログラム(EGLコンテキスト生成と1枚だけ単純な図形をレンダリングするもの)を作成してGoogleに「どうなってるんだ?」と詰め寄る問い合わせをする準備を進めていました。

別の作業もあり2週間ほどの時間が経過していたのですが、問い合わせをする前にあらためて実行してみると…
何と0.06秒でコンテキスト生成が完了するではありませんか!

ベータ期間だったということもあってか、その間にコンテキスト生成のオーバーヘッドの問題が改善されていたようです。これは嬉しい驚きで、晴れてGPUのパワーを享受できることになりました。

パフォーマンス比較

今回、インスタンスに選択した構成はNVIDIAのK80を使用するもので、実際にポリゴン数が134万で頂点カラーを持つモデルをレンダリングして性能計測をした結果は以下の通りです。比較のためにMesa(CPUのみで実行)を使った場合とローカルのマシンでの結果も上げておきます。

GCP インスタンスのスペック
マシンタイプ: n1-standard-4-k80x1
CPU: Intel(R) Xeon(R) CPU @ 2.50GHz x 4
メモリ: 15GB
GPU: NVIDIA Tesla K80 x 1

ローカルマシンのスペック
CPU: Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz x 8
メモリ: 32GB
GPU: NVIDIA GeForce GTX980Ti x 1

30フレーム分の画像のレンダリング時間 (単位:秒)

シャドウ OFF レンダリング jpegエンコードと保存 他の処理も含む合計 レンダリング性能比
local (GPU GTX980i) 0.132808 0.185638 0.483039 68.17031354
local (CPU) 6.267509 0.183637 6.570684 1.444523335
GCP (GPU K80) 0.186132 0.772457 1.354518 48.64055079
GCP (CPU) 9.053563 0.756659 10.072877 1
シャドウ ON レンダリング jpegエンコードと保存 他の処理も含む合計 レンダリング性能比
local (GPU GTX980i) 0.167166 0.184854 0.538712 166.2302861
local (CPU) 13.55353 0.183102 13.857921 2.050244623
GCP (GPU K80) 0.247529 0.776322 1.398778 112.2618037
GCP (CPU) 27.788052 0.736659 28.80428 1



当たり前のことではありますがGPUを使用した場合の性能には圧倒的な差があり、特にシャドウをONにした際に顕著です。
その一方で、ローカルマシンのGTX980TiよりGCPのインスタンス上のK80の方が遅いという結果が出ていることが気になりますが、localとGCPのレンダリング性能比を計算してみると、それぞれ、
・CPUレンダリング: 9.053563 / 6.267509 = 1.444523
・GPUレンダリング: 0.186132 / 0.132808 = 1.401511
となります。OpenGLでレンダリングする際の処理は全てGPUで処理されるわけではなく、OpenGLのドライバ内でCPUにて処理する部分があり、今回の測定環境であるlocalとGCPのCPUクロック比が 4.0GHz / 2.5GHz = 1.6 という差があることから考えると、CPUの性能差が影響しているのではないかと推測します。

今後の課題

GPUを活用することで高速なレンダリングができるようにはなりましたが、性能の特製をつかむためにもレンダリングにかかる時間におけるGPUとCPUの正確な比率の分析、および「jpegエンコードと保存」の値においてはlocalとGCPの間でCPUの性能差、ストレージの性能差(両方ともSSD)以上の開きがあるため、jpegエンコード時のGPU活用度等の観点でさらなる検証が必要です。

最後に

今回はOpenGLを使ってレンダリング処理を高速化しましたが、3Dモデルデータの他の解析処理もCUDAを使って実装することでGPUを活用して高速化する計画が進行中です。現在この辺りの実装は私を含む2名体制で進めていますが、引き続きカブクではそういった技術を活用して一緒に速さを追い求めてくれるエンジニアを募集しております。



※ この画像はGCPでGPUを使用したバージョンのエンジンで3方向からの光源を配置し、8xのマルチサンプルのアンチエイリアスをかけて、フレーム毎にY軸で回転させて30枚の連続画像としてレンダリングしたものです。

その他の記事

Other Articles

2022/06/03
拡張子に Web アプリを関連付ける File Handling API の使い方

2022/03/22
<selectmenu> タグできる子; <select> に代わるカスタマイズ可能なドロップダウンリスト

2022/03/02
Java 15 のテキストブロックを横目に C# 11 の生文字列リテラルを眺めて ECMAScript String dedent プロポーザルを想う

2021/10/13
Angularによる開発をできるだけ型安全にするためのKabukuでの取り組み

2021/09/30
さようなら、Node.js

2021/09/30
Union 型を含むオブジェクト型を代入するときに遭遇しうるTypeScript型チェックの制限について

2021/09/16
[ECMAScript] Pipe operator 論争まとめ – F# か Hack か両方か

2021/07/05
TypeScript v4.3 の機能を使って immutable ライブラリの型付けを頑張る

2021/06/25
Denoでwasmを動かすだけの話

2021/05/18
DOMMatrix: 2D / 3D 変形(アフィン変換)の行列を扱う DOM API

2021/03/29
GoのWASMがライブラリではなくアプリケーションであること

2021/03/26
Pythonプロジェクトの共通のひな形を作る

2021/03/25
インラインスタイルと Tailwind CSS と Tailwind CSS 入力補助ライブラリと Tailwind CSS in JS

2021/03/23
Serverless NEGを使ってApp Engineにカスタムドメインをワイルドカードマッピング

2021/01/07
esbuild の機能が足りないならプラグインを自作すればいいじゃない

2020/08/26
TypeScriptで関数の部分型を理解しよう

2020/06/16
[Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

2020/03/19
[Web フロントエンド] Elm に心折れ Mint に癒しを求める

2020/02/28
さようなら、TypeScript enum

2020/02/14
受付のLooking Glassに加えたひと工夫

2020/01/28
カブクエンジニア開発合宿に行ってきました 2020冬

2020/01/30
Renovateで依存ライブラリをリノベーションしよう 〜 Bitbucket編 〜

2019/12/27
Cloud Tasks でも deferred ライブラリが使いたい

2019/12/25
*, ::before, ::after { flex: none; }

2019/12/21
Top-level awaitとDual Package Hazard

2019/12/20
Three.jsからWebGLまで行きて帰りし物語

2019/12/18
Three.jsに入門+手を検出してAR.jsと組み合わせてみた

2019/12/04
WebXR AR Paint その2

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く