• トップ
  • G-DEPについて
  • ご購入ガイド
  • サポート
  • お問い合わせ

G-DEPトップ  >  G-DEPの高速演算記  >  高速演算記 第27回 「FA向けGPU対応画像処理ライブラリの開発現場より」

高速演算記 第27回 「FA向けGPU対応画像処理ライブラリの開発現場より」

今回の高速演算記第27回は、株式会社ファースト 技術開発本部 柏谷俊輔様より、コンピュータ制御技術を用いて工場を自動化する “Factory Automation(FA)” のための画像処理ライブラリをCUDA化する取組みについてご紹介頂きました。幅広いアーキテクチャへの対応を要求されるという、ライブラリ開発現場ならではの内容となっております。是非ご一読下さい。

執筆:株式会社ファースト 技術開発本部 柏谷俊輔様

 

1. はじめに

 私たち株式会社ファーストは日本の画像処理メーカーです。1982年の創業以来、画像処理ライブラリ、汎用画像処理装置、画像入力ボードといった自社開発製品やソリューション事業などを通してFAでの生産効率を高めることに寄与してきました。
 FA向けの画像処理では常に高速化が求められ、これまでマルチコアCPUやFPGAなどを使用してこれを実現してきました。一方HPCなどの分野では、このコラムではおなじみのGPUコンピューティングを利用した高速化が広まっています。私たちの分野でもこの技術を利用して、さらなる高速化の実現ができるのではないでしょうか。

 本稿では、私たちが行っているFA 分野でのGPUを用いた画像処理高速化の取り組みについて紹介します。

 

2. GPU対応画像処理ライブラリの開発

 私たちはGPUを使った画像処理の高速化をCUDA を用いて行っています。そして、これを様々なアプリケーションで利用が可能な「ライブラリ」という形でリリースを行いました。現時点のライブラリ機能としては、カラー・濃淡・2値の画像操作、色空間変換、幾何変換、空間フィルタ、モフォロジ、画像間演算、論理演算、パターンマッチング等の画像処理が含まれています。

 今回はこの中から比較的単純な処理である空間フィルタを例として、ライブラリ開発時の留意点などを紹介します。

 

空間フィルタの例

 ここで例とする空間フィルタは、入出力用として1枚ずつ与えられた二次元画像の各画素に対して入力画像上の注目画素とその近傍画素(上下左右など)の画素値から出力画像の新たな画素値を算出するような処理とします。
 

 
 このような空間フィルタでは画像全体に対する処理が画素ごとに独立したものとなるため、GPUコンピューティングによる並列化に向いています。しかし、画素値を算出するための演算負荷が低い場合には特に、「各画素へスレッドをどのようにマッピングするか」という実装上の方針によってパフォーマンスが大きく変わってくる傾向にあります。これは演算負荷の低い処理がメモリアクセスに律速されてくる上に、そのメモリアクセスの効率を左右するアクセスパターンがスレッドマッピングにより決まるためです。最高のパフォーマンスを得るべく最適なスレッドマッピング戦略を考えていくと、これを実現するためにはGPUのアーキテクチャや処理対象となるデータサイズを考慮する必要があることに気づきます。

 一方、画像処理ライブラリはユーザーが作成する様々なアプリケーションから利用されるソフトウェアコンポーネントであるという性質上次のような使命をもちます。

     「ライブラリは様々な世代・スペックのGPUボードを実行ターゲットとして実行される」
     「ライブラリは様々なサイズ(数キロバイトや、数百メガバイトなど)のデータを処理対象として実行される」

実行ターゲットとなるGPUボードが1種類のみで処理対象となるデータの量もある程度決まっているような専用システム向けアプリケーションでは、開発時のチューニング段階で最適なスレッドマッピング戦略を決定しておくことも可能です。しかし様々な世代・スペックのGPUボードや様々なサイズの処理対象データに対応することを求められるライブラリでは、実行する状況に応じて最適なスレッドマッピングを行うような仕組みを用意しておく必要があります。

 では具体的にどのようなことが必要となってくるのか少し詳しく見ていきます。

 

スレッド数の決定

 まず1つのスレッドブロックの中にはいくつのスレッドを含ませるのが最適であるかを考えます。今回の単純な空間フィルタのような例の場合、1ブロック中のスレッド数はカーネル実行時のマルチプロセッサ内に保持されるブロック数・スレッド数を可能な限り多くできるようなものにすることが、メモリアクセスのレイテンシを隠蔽する意味でも最適となることが多いので、これを目指してみます。

 このようなスレッド数を求めるためには、マルチプロセッサのリソース量とカーネルが使用するリソース量が必要となります。マルチプロセッサのリソース量は実行ターゲットとなるGPUの世代によって異なります。ライブラリでは様々な世代のGPUをターゲットとするため、実行時にCUDAのAPI(ランタイム関数cudaGetDeviceProperties()など)を使用してマルチプロセッサのリソース量を取得する必要があります。

 カーネルが使用するリソース量として必要となるのはレジスタ数と共有メモリ量です。レジスタ数に関してはコンパイル時にnvcc から出力されるログで確認することもできますが、コードを生成するアーキテクチャ(sm_20やsm_30など)や、CUDA のバージョンによってレジスタ数は異なってくる場合もありますので、これも実行時にCUDA のAPI(ランタイム関数cudaFuncGetAttributes() など)を使用して取得します。

 こうして実行時に取得したマルチプロセッサのリソース量とカーネルが使用するリソース量をもとに最適なスレッド数の計算を行います。この計算は、マルチプロセッサの最大保持スレッド数(Maximum number of resident threads per multiprocessor)を最大保持ブロック数(Maximum number of resident blocks per multiprocessor)で除算するというような単純なものとなる場合もあります。しかしカーネルが使用するリソース(レジスタと共有メモリ)量が多い場合には、マルチプロセッサに搭載されているリソース量に制限されることにより保持出来るブロック数・スレッド数が減ってしまう「パフォーマンスクリフ」と呼ばれる状況が発生する可能性もあるので、これを避けつつスレッド数を決定します。

 このように、ライブラリでは最適なスレッド数を決定するために必要となる情報を実行時まで得ることが出来ないため、動的に必要な情報を取得しスレッド数の決定を行う必要があります。

 

スレッドと画素の関係

 1ブロック中のスレッド数を決めることが出来たので、次は各スレッドに画像上のどの位置の画素を処理させるかを考えます。画像データがグローバルメモリ上に置かれている場合、効率的なメモリアクセスを実現するためには「コアレスアクセス」や「キャッシュ効率」などを考慮してスレッドを各画素に割り当てる必要があります。水平方向に連続した画素はリニアメモリ上で連続した配置になるのに対し、同一ワープ内のスレッドによるメモリアクセスがコアレッシングされる可能性を考えると、同一ワープ内のスレッドは水平方向に連続した画素へ割り当てることが効果的ということになります。

 さらに各マルチプロセッサには個別にL1キャッシュが搭載されている(Compute Capability 2.0 以降) のに対し、1つのスレッドブロックは1つのマルチプロセッサに割り当てられることを考えると、同一ブロック内のスレッドを水平方向に連続した画素へ割り当てることによりキャッシュ効率を高めることが出来そうです。

 こうして考えていくと、1つのブロックに含まれるスレッドは水平方向に連続した画素へ割り当てるものとし、このようなブロックを画像全体が埋め尽くされるように配置することが最適な方法ということになります。
 

 

 しかしこの方法では、処理する画像のサイズ次第で画像の範囲外にマッピングされるスレッドの数が多くなってしまい、パフォーマンスに悪影響を与える場合があります。例えば1ブロックに含まれる128個のスレッドで水平方向に連続した128個の画素を処理させようとした場合を考えてみます。

 まず処理対象となる画像のサイズが512 ×480 画素である場合、必要なブロックの数は4 ×480 = 1,920 個となります。このとき1,920 ×128 = 245,760 個のスレッドで512 ×480 = 245,760 画素を処理することになるので全てのスレッドに処理を与えることが出来ます。
 
 これに対し画像サイズが513 ×480 画素であった場合には、水平方向に4 個のブロックではスレッドが足りないので5 個のブロックが必要となり、画像全体では5 ×480 = 2,400 個のブロックを使用することになります。このとき2,400 ×128 = 307,200 個のスレッドで513 ×480 = 246,240 画素を処理することになるので307,200 – 246,240 = 60,960 個のスレッドには処理を与えることが出来ません。つまり無駄なスレッドの数が多くなってしまいパフォーマンスに悪影響を及ぼす可能性が出てきます。

 
 では同じ128個のスレッドで水平方向に32 画素、垂直方向に4 画素の計128 画素を処理させようとした場合はどうなるでしょうか。この方法では513 ×480 画素の画像を処理するのに必要なブロックの数は17 ×120 = 2,040 個となり、無駄となるスレッドの数は14,880 個となります。これでもまだ無駄なスレッドは発生してしまいますが、先の方法と比べると約1/4 に減らすことが出来たのでパフォーマンスの改善が期待出来ます。

 このようにスレッドの割り当て方で無駄となるスレッドの数は変わってくるので、画像サイズに応じて最適な方法を選択する必要があります。またこれまでは1つのスレッドに1個の画素を処理させることを考えてきましたが、1つのスレッドに複数個の画素を処理させることでパフォーマンスを改善出来る場合があります。1つのスレッドに隣接した画素を連続して処理させることにより、前回の画素値や計算結果を使いまわすことが出来る場合は特に効果を期待出来ます。しかし1つのスレッドに処理させる画素の個数が多すぎると並列数が低くなるため、画像サイズが小さい場合には不利ということになります。この場合もやはり、画像サイズに応じて1つのスレッドに処理させる画素の個数を調整する必要があります。

 このようにスレッドを画素に割り当てる方法は処理の内容によって色々考えられますが、何れの場合でもパフォーマンスを向上させるために最適となる方法は処理対象となる画像サイズによって異なってきます。そのため様々な画像サイズで最適なパフォーマンスを求められるライブラリでは、前述のような最適となる方法の選択を画像サイズに応じて動的に行っています。

 

その他

 この他、画像処理では「局所領域のリダクション結果を複数画素で共有する」、または「隣接する画素間で途中計算結果を共有する」といったことが効率の良い処理となる場合があります。このような場合、「スレッド間の明示的な同期を最小限に減らす」といったワープサイズに依存した最適化がパフォーマンス向上につながることがあります。

 このとき注意したいのは、ワープサイズに依存するということは実行ターゲットとなるGPUの世代を限定してしまっているということです。現時点で存在する全てのCompute Capability (1.0 ~3.5)でワープサイズは同じ値となっています。しかし将来の世代でワープサイズが変わらないと明言されてはいないようなので、実行時にワープサイズを取得する(またはターゲットとするアーキテクチャで生成するコードを切り替える)といった対応を行っておいて損はないはずです。

 ここまで空間フィルタの例などを通して、私たちが画像処理ライブラリの開発を進める中で意識している事柄をいくつか紹介してきました。この中で特に強調していた「GPUのアーキテクチャや、データサイズに応じて動的にチューニングを行う」というような事柄は、様々な世代のGPU がもつ演算能力を可能な限り利用するということを目的としています。GPU コンピューティングを利用する他の分野においては、データサイズや演算負荷が極めて大きなものである場合には特に、このような事柄をあまり意識する必要の無い(パフォーマンスに与える影響が極めて低い)場合もあるでしょう。しかしFA向け画像処理が対象とするようなデータサイズや演算負荷の範囲では、このような事柄を意識することでパフォーマンスを向上させることが出来る場合が多いようです。

 FAで行われる画像処理の中にはGPUのアーキテクチャに向いているものが多数あります。そして今後カメラの高解像度化や高フレームレート化が進むのにも伴ってGPU のような演算能力を必要とする場面も増えてくるのではないでしょうか。また、演算量が極めて高くCPU で処理させるには現実的ではなかったような処理が、GPU を使うことにより現実的となる場面もあるのではないでしょうか。私たちはこの技術をFAでの生産効率を高める手段の一つとして活用していこうと取り組んでいます。

 

3. おわりに

 私たちは従来からCPU上で動作するWindows用画像処理ライブラリ「WIL」をリリースしています。WIL はC#(.NET Framework)でのGUIアプリケーション開発に好適なクラスライブラリとなっています。WIL の中の画像処理部分はC言語で記述されています。この部分は「FIE (FAST Image Engine)」というC言語関数群からなるライブラリとして独立しておりアプリケーションから直接利用することも可能です。

 WILの画像処理はFIE内部で行われている並列化やベクトル化などの最適化によりCPU上で高速に動作しますが、GPU上で動作するライブラリを使用することでさらなる高速化を実現することが出来ます。このGPU上で動作するライブラリは、オプションライブラリ「FGA (FAST GPU Accelerator) 」という形でWILに含まれています。FGAは先述したFIEと同様C言語関数群からなるライブラリとなっており、その関数インターフェースはFIEと互換性をもっているためFIEとの併用や移行を容易に行うことが出来ます。

 FGA内部のGPU処理はCUDAを用いて実装されていますが、FGAを使用するユーザーはCUDAやGPUについての知識を必要としません。C言語のインターフェースでライブラリを使用するだけで、今回のコラムで紹介したような内容をはじめとする様々な工夫により実現された「GPUコンピューティングによる画像処理高速化」の成果を手に入れることが出来るのです。
 

 
詳しくは私たちのWebサイトで。
 
 
以上