More Related Content Similar to Halide による画像処理プログラミング入門 (20) More from Fixstars Corporation (20) Halide による画像処理プログラミング入門9. C++でのナイーブな実装コード
3x3Blurフィルターの実装例
8
void box_filter_3x3(const Image in, Image &blury) {
Image blurx(in.width(), in.height());
for (int y = 0; y < in.height(); y++)
for (int x = 0; x < in.width(); x++)
blurx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3;
for (int y = 0; y < in.height(); y++)
for (int x = 0; x < in.width(); x++)
blury(x, y) = (blurx(x, y-1) + blurx(x, y) + blurx(x, y+1))/3;
}
10. C++でのIntel向け手動最適化コード
9
void box_filter_3x3(const Image &in, Image &blury) {
__m128i one_third = _mm_set1_epi16(21846);
#pragma omp parallel for
for (int yTile = 0; yTile < in.height(); yTile += 32) {
__m128i a, b, c, sum, avg;
__m128i blurx[(256/8)*(32+2)]; // allocate tile blurx array
for (int xTile = 0; xTile < in.width(); xTile += 256) {
__m128i *blurxPtr = blurx;
for (int y = -1; y < 32+1; y++) {
const uint16_t *inPtr = &(in[yTile+y][xTile]);
for (int x = 0; x < 256; x += 8) {
a = _mm_loadu_si128((__m128i*)(inPtr-1));
b = _mm_loadu_si128((__m128i*)(inPtr+1));
c = _mm_load_si128((__m128i*)(inPtr));
sum = _mm_add_epi16(_mm_add_epi16(a, b), c);
avg = _mm_mulhi_epi16(sum, one_third);
_mm_store_si128(blurxPtr++, avg);
inPtr += 8;
}
}
blurxPtr = blurx;
for (int y = 0; y < 32; y++) {
__m128i *outPtr = (__m128i *)(&(blury[yTile+y][xTile]));
for (int x = 0; x < 256; x += 8) {
a = _mm_load_si128(blurxPtr+(2*256)/8);
b = _mm_load_si128(blurxPtr+256/8);
c = _mm_load_si128(blurxPtr++);
sum = _mm_add_epi16(_mm_add_epi16(a, b), c);
avg = _mm_mulhi_epi16(sum, one_third);
_mm_store_si128(outPtr++, avg);
}
}
}
}
}
11. C++でのIntel向け手動最適化コード
10
void box_filter_3x3(const Image &in, Image &blury) {
__m128i one_third = _mm_set1_epi16(21846);
#pragma omp parallel for
for (int yTile = 0; yTile < in.height(); yTile += 32) {
__m128i a, b, c, sum, avg;
__m128i blurx[(256/8)*(32+2)]; // allocate tile blurx array
for (int xTile = 0; xTile < in.width(); xTile += 256) {
__m128i *blurxPtr = blurx;
for (int y = -1; y < 32+1; y++) {
const uint16_t *inPtr = &(in[yTile+y][xTile]);
for (int x = 0; x < 256; x += 8) {
a = _mm_loadu_si128((__m128i*)(inPtr-1));
b = _mm_loadu_si128((__m128i*)(inPtr+1));
c = _mm_load_si128((__m128i*)(inPtr));
sum = _mm_add_epi16(_mm_add_epi16(a, b), c);
avg = _mm_mulhi_epi16(sum, one_third);
_mm_store_si128(blurxPtr++, avg);
inPtr += 8;
}
}
blurxPtr = blurx;
for (int y = 0; y < 32; y++) {
__m128i *outPtr = (__m128i *)(&(blury[yTile+y][xTile]));
for (int x = 0; x < 256; x += 8) {
a = _mm_load_si128(blurxPtr+(2*256)/8);
b = _mm_load_si128(blurxPtr+256/8);
c = _mm_load_si128(blurxPtr++);
sum = _mm_add_epi16(_mm_add_epi16(a, b), c);
avg = _mm_mulhi_epi16(sum, one_third);
_mm_store_si128(outPtr++, avg);
}
}
}
}
}
12. Halideでのアルゴリズム実装
11
Func box_filter_3x3(Func in) {
Func blurx, blury;
Var x, y;
blurx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3;
blury(x, y) = (blurx(x, y-1) + blurx(x, y) + blurx(x, y+1))/3;
return blury;
}
13. Halideでのアルゴリズム実装
12
Func box_filter_3x3(Func in) {
Func blurx, blury;
Var x, y;
blurx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3;
blury(x, y) = (blurx(x, y-1) + blurx(x, y) + blurx(x, y+1))/3;
return blury;
}
アルゴリズム記述部
アルゴリズムは本質的な処理の内容だけを記述
14. HalideでのIntel向け最適化実装
13
Func box_filter_3x3(Func in) {
Func blurx, blury;
Var x, y;
blurx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3;
blury(x, y) = (blurx(x, y-1) + blurx(x, y) + blurx(x, y+1))/3;
blury.tile(x, y, xi, yi, 256, 32).vectorize(xi, 8).parallel(y);
blurx.compute_at(blury, x).store_at(blury, x).vectorize(x, 8);
return blury;
}
少ないコード量と期間で簡単に最適化が可能
スケジューリング記述部
アルゴリズムは変更なし
15. マルチプラットフォーム向けの最適化
14
Func box_filter_3x3(Func in) {
Func blurx, blury;
Var x, y;
blurx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3;
blury(x, y) = (blurx(x, y-1) + blurx(x, y) + blurx(x, y+1))/3;
if (target.has_gpu_feature()) {
Var tx, ty;
blury.gpu_tile(x, y, tx, ty, 32, 8);
} else {
blury.tile(x, y, xi, yi, 256, 32).vectorize(xi, 8).parallel(y);
blurx.compute_at(blury, x).store_at(blury, x).vectorize(x, 8);
}
return blury;
}
複数のハードウェアに対しても簡単に最適化が可能
GPU用のスケジューリング記述
22. 関数の定義
アルゴリズムは純粋関数として定義される
– 関数: Halide::Func
– 次元変数: Halide::Var
21
Func f;
Var x, y;
f(x, y) = x + y;
意味: 関数f は定義域 x, yに対して x + y を値域に持つ
名前空間 Halide:: は以下省略
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
f[y][x] = x + y;
}
}
等価なC++ソースコードHalideで書かれたソースコード
24. 関数の次元数
最大4次元までの関数を定義可能
23
Func f;
Var c, x, y;
f(c, x, y) = c + x + y;
Func f;
Var c, x, y, z;
f(c, x, y, z) = c + x + y + z;
fは3次元の関数
(e.g. 3D空間, カラー画像)
fは4次元の関数
28. RDomと畳み込み関数
リダクションドメイン: Rdom
– RDom(min, extent)
– 指定された次元で[min, min+extent-1]の領域を動く
ループ変数
27
rx は[-1, 1] を動くループ変数
Func f, g;
Var x;
RDom rx(-1, 3)
f(x) = x;
g(x) = sum(f(x + rx));
for (int x=0; x<width; x++) {
T sum = 0;
for (int rx=-1; rx<2; rx++) {
sum += x + rx;
}
g[x] = sum;
}
等価なC++ソースコード
29. RDomと畳み込み関数
畳み込み関数:
– RDomで指定した領域を畳み込んで演算を行う関数
• 例: sum/product/maximum/minimum/argmin/argmax
28
Func f, g;
Var x;
RDom rx(-1, 3)
f(x) = x;
g(x) = sum(f(x + rx));
関数 g は各xにおいて、x-1, x+0, x+1の総和を値域に持つ
for (int x=0; x<width; x++) {
T sum = 0;
for (int rx=-1; rx<2; rx++) {
sum += x + rx;
}
g[x] = sum;
}
等価なC++ソースコード
30. 領域外アクセス時の参照値設定
BoundaryConditions
– constant_exterior: 指定した定数値を参照
– repeat_edge: 袖領域を拡張して参照
– mirror_image: 画像を反転して複製するように参照
– etc…
29
Func f, f_, g;
Var x;
RDom rx(-1, 3)
f(x) = x;
f_ = BoundaryConditions::constant_exterior(f, 0)
g(x) = sum(f_(x + rx));
関数 f は定義域 x<0 または x>=width のとき、値 0 を返す
32. 2次元Convolution
31
𝐷𝑠𝑡 𝑥, 𝑦 =
𝑘𝑦=0
𝑘ℎ
𝑘𝑥=0
𝑘𝑤
𝑆𝑟𝑐 𝑥 + 𝑘𝑥 −
𝑘𝑤
2
, 𝑦 + 𝑘𝑦 −
𝑘ℎ
2
× 𝐾𝑒𝑟𝑛𝑒𝑙(𝑘𝑥, 𝑘𝑦)
2次元画像の畳み込み処理
– 画像処理において頻繁に用いられる処理
= ∗
33. 2次元Convolution
32
例: 画像処理フィルタ
– ぼかしやエッジ検出、ノイズ除去などの様々な
画像処理に使用されている
1
16
1
8
1
16
1
8
1
4
1
8
1
16
1
8
1
16
−1 −1 −1
−1 8 −1
−1 −1 −1
=
∗
=
Gaussian Kernel
Laplacian Kernel
ぼかし処理
エッジ検出
34. DeepLearningにおけるConvolution
Convolutional Neural Network
– 画像認識に使用されるニューラルネットワーク
– 推論時はConvolution層が全体の処理時間の50%以上を占める
• 最近のネットワークではConvolution層の多段化により
さらに処理時間が増加
Convolution層の高速化はとても重要!
33
Convolution層
LeNetのネットワーク図
Y. LeCun, L. Bottou, Y. Bengio, and P. Haffner. Gradient-based learning
applied to document recognition. Proceedings of the IEEE, november 1998.
35. 2次元Convolutionのアルゴリズム記述
C++での実装例
34
void conv3x3(const uint8_t* src, const float* kernel, uint8_t* dst,
int height, int width) {
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
float tmp = .0f;
for (int ky=0; ky<kh; ky++) {
for (int kx=0; kx<kw; kx++) {
tmp += src[y+ky-kh/2][x+kx-kw/2] * kernel[ky][kx];
}
}
dst[y][x] = tmp;
}
}
}
36. 2次元Convolutionのアルゴリズム記述
Generator インタフェースに従った記述例
– ターゲットや出力形式、コンパイル時パラメータを
コンパイル時に指定可能
35
class conv3x3 : public Generator<conv3x3>{
Var x{"x"}, y{"y"};
public:
Input<Buffer<uint8_t>> src{“src", 2};
Input<Buffer<float>> kernel{"kernel", 2};
Output<Buffer<uint8_t>> dst{“dst", 2};
void generate() {
RDom r(-1, 3, -1, 3, "r");
Func src_ = BoundaryConditions::repeat_edge(in);
dst(x, y) = cast<uint8_t>(sum(cast<float>(src_(x+r.x, y+r.y)) * kernel(r.x+1, r.y+1)));
}
};
HALIDE_REGISTER_GENERATOR(conv3x3, conv3x3)
37. 2次元Convolutionのアルゴリズム記述
Generator インタフェースに従った記述例
– ターゲットや出力形式、コンパイル時パラメータを
コンパイル時に指定可能
36
class conv3x3 : public Generator<conv3x3>{
Var x{"x"}, y{"y"};
public:
Input<Buffer<uint8_t>> src{“src", 2};
Input<Buffer<float>> kernel{"kernel", 2};
Output<Buffer<uint8_t>> dst{“dst", 2};
void generate() {
RDom r(-1, 3, -1, 3, "r");
Func src_ = BoundaryConditions::repeat_edge(in);
dst(x, y) = cast<uint8_t>(sum(cast<float>(src_(x+r.x, y+r.y)) * kernel(r.x+1, r.y+1)));
}
};
HALIDE_REGISTER_GENERATOR(conv3x3, conv3x3)
入出力パラメータ
アルゴリズムの記述部
Generatorを登録
40. 計算タイミングの指定
Func::compute_inline (デフォルト)
– 出力のFunc以外はインライン展開される
• 〇必要メモリ量は少なくなる
• ×計算量が増える可能性あり
39
等価
Func blur_x, blur_y;
Var x, y;
blur_x(x, y) =
in(x, y) + in(x+1, y);
blur_y(x, y) =
(blur_x(x, y) + blur_x(x, y+1)) / 4;
Func blur_y;
Var x, y;
blur_y(x, y) =
(in(x, y) + in(x+1, y) +
in(x, y+1) + in(x+1, y+1)) / 4;
41. 計算タイミングの指定
Func::compute_root
– 関数の全領域の計算結果がメモリに保持される
• 〇計算量が削減できる可能性あり
• ×必要メモリ量は多くなる
40
Func blur_x, blur_y;
Var x, y;
blur_x(x, y) =
in(x, y) + in(x+1, y);
blur_y(x, y) =
(blur_x(x, y) + blur_x(x, y+1)) / 4;
blur_x.compute_root();
blur_xはblur_yを評価する前に全領域が計算される
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
blur_x[y][x] = in[y][x] + in[y][x+1];
}
}
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
blur_y[y][x] =
(blur_x[y][x] + blur_x[y+1][x]) / 4;
}
}
42. 計算タイミングの指定
Func::compute_at
– 指定した関数の次元のループ内で必要な領域のみの
計算結果がメモリに保持される
• 計算量・メモリ使用量共に
compute_inlineとcompute_rootの中間となる
41
blur_xはblur_yのyループ内で必要な領域が計算される
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
blur_x[0][x] = in[y][x] + in[y][x+1];
blur_x[1][x] =
in[y+1][x] + in[y+1][x+1];
}
for (int x=0; x<width; x++) {
blur_y[y][x] =
(blur_x[0][x] + blur_x[1][x]) / 4;
}
}
Func blur_x, blur_y;
Var x, y;
blur_x(x, y) =
in(x, y) + in(x+1, y);
blur_y(x, y) =
(blur_x(x, y) + blur_x(x, y+1)) / 4;
blur_x.compute_at(blue_y, y);
45. ベクトル化
Func::vectorize
– 指定した次元とベクトル幅でループをベクトル化
ベクトル化
– SIMD命令を使用するように最適化すること
• 1命令で複数のデータを同時に演算可能
• 例: Intel SSE/AVX, ARM NEON, Power AltiVec など
44
Func f, g, h;
Var x;
f(x) = g(x) + h(x);
f.vectorize(x, 4);
+
g
h
f
1命令で4要素が同時に演算される
Vectorize
46. ループアンロール
Func::unroll
– 指定した次元に対してループ展開を行う
• 分岐命令の削減
• ソフトウェアパイプライニング
• レジスタの再利用(レジスタブロッキング)
45
for (int y=0; y<height; y++)
for (int x=0; x<width; x++)
f[y][x] = x + y;
unroll前の等価なC++ソースコード
Func f;
Var x, y;
f(x, y) = x + y;
47. ループアンロール
Func::unroll
– 指定した次元に対してループ展開を行う
• 分岐命令の削減
• ソフトウェアパイプライニング
• レジスタの再利用(レジスタブロッキング)
46
Func f;
Var x, y;
f(x, y) = x + y;
f.unroll(x, 2);
for (int y=0; y<height; y++)
for (int x=0; x<width; x+=2) {
f[y][x] = x + y;
f[x+1][y] = x+1 + y;
}
for (int y=0; y<height; y++)
for (int x=0; x<width; x++)
f[y][x] = x + y;
unroll前の等価なC++ソースコード
Unroll後の等価なC++ソースコード
Unroll
48. タイリング
Func::tile
– 指定した次元とタイルサイズでループをタイル化する
• データの再利用性を高める
⇒キャッシュやローカルメモリなどを有効活用できる
47
Func f;
Var x, y;
f(x, y) = in(y, x);
f.tile(x, y, xi, yi, 16, 16);
for (int y=0; y<height; y+=16) {
for (int x=0; x<width; x+=16) {
for(int yi = 0; yi < 16; yi++) {
for(int xi = 0; xi < 16; xi++) {
f[y+yi][x+xi] = in[x+xi][y+yi];
}
}
}
}
Tile後の等価なC++ソースコード
50. 2次元Convolutionのスケジュール記述
Generator インタフェースに従った記述例
– schedule関数内にスケジューリングを記述
49
class conv3x3 : public Generator<conv3x3>{
Var x{"x"}, y{"y"};
public:
Input<Buffer<uint8_t>> src{“src", 2};
Input<Buffer<float>> kernel{"kernel", 2};
Output<Buffer<uint8_t>> dst{"dst", 2};
void generate() {
RDom r(-1, 3, -1, 3, "r");
Func src_ = BoundaryConditions::repeat_edge(src);
dst(x, y) = cast<uint8_t>(sum(cast<float>(src_(x+r.x, y+r.y)) * kernel(r.x+1, r.y+1)));
}
void schedule() {
}
};
スケジューリングを記述
51. 性能評価環境
評価環境
– CPU: Intel Core i7-5930K
• 周波数: 3.5GHz
• 6コア12スレッド
• AVX2/FMA3搭載(1命令で256-bit幅の積和演算が可能)
– コンパイラ
• GCC-6.3.0 (C++実装用のコンパイラとして使用)
• Halide: Release-2017_10_31 & LLVM-5.0.0
• コンパイルオプション: -O3 –march=native
2次元Convolution
– 入出力: 2048x2048 8-bitグレースケール画像
– カーネルサイズ: 3x3
– カーネル: Gaussianカーネル
50
55. ループ展開によるレジスタブロッキング
54
スケジューリングの記述
void generate() {
RDom r(-1, 3, -1, 3, "r");
Func src_ = BoundaryConditions::repeat_edge(src);
dst(x, y) =
cast<uint8_t>(sum(cast<float>(src_(x+r.x, y+r.y)) * kernel(r.x+1, r.y+1)));
}
void schedule() {
out.split(y, yo, yi, 4);
out.reorder(yi, x, yo);
out.unroll(yi);
}
Y次元で4展開分のループアンロール
57. スレッド並列化
Yの次元でスレッド並列化を適用
56
void generate() {
RDom r(-1, 3, -1, 3, "r");
Func src_ = BoundaryConditions::repeat_edge(src);
dst(x, y) =
cast<uint8_t>(sum(cast<float>(src_(x+r.x, y+r.y)) * kernel(r.x+1, r.y+1)));
}
void schedule() {
out.split(y, yo, yi, 4);
out.reorder(yi, x, yo);
out.unroll(yi);
out.parallel(yo);
}
Yの次元でスレッド並列化
63. Google Pixel2
62
Googleが発表したスマートフォン
画像処理・機械学習用のカスタムSoC
「Pixel Visual Core」を搭載
– 8個のIPU(Image Processing Unit)を搭載
– IPUはTensorFlowとHalideによる開発をサポート
https://techcrunch.com/2017/10/17/googles-first-custom-consumer-chip-is-the-secret-behind-the-pixel-2s-camera-performance
67. Halide to FPGA 概要
GPUCPU FPGA
GenesisコンパイラがHalide ベースのプログラムコードを
FPGA向けのコードへと変換します
Halideアプリケーション
プログラム
Genesis コンパイラ
© 2017 Fixstars Corp. CONFIDENTIAL
66