Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Genesis コンパイラへの Z3 の導入

1,615 views

Published on

コンパイラ勉強会@Fixstars の資料です

Published in: Software
  • Be the first to comment

Genesis コンパイラへの Z3 の導入

  1. 1. Genesis コンパイラへの Z3 の導入 朝倉 泉 (株式会社フィックスターズ)
  2. 2. 発表内容 Genesis のストリーム化変換について紹介 – Genesis: Fixstars で開発中の Halide から FPGA 回路への コンパイラ • https://www.halide2fpga.com/ • Halide: 画像処理アルゴリズムに特化した C++ の埋め込みDSL • Vivado HLS で合成可能な C++ プログラムを生成 – Z3 によるウィンドウレジスタサイズ推論 1
  3. 3. アウトライン Halide Genesis – ストリーム処理 ウィンドウサイズ推論 – 問題点 – Z3の導入 – 評価 まとめ 2
  4. 4. Halide 画像処理用 C++ 埋め込み DSL 3 Func blur_3x3(Func in) { Func bx, by; Var x, y, xi, yi; bx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3; by(x, y) = (bx(x, y-1) + bx(x, y) + bx(x, y+1))/3; by.tile(x, y, xi, yi, 256, 32) .vectorize(xi, 8).parallel(y); bx.compute_at(by, x).vectorize(x, 8); return by; } 計算の定義 スケジューリング (最適化の方針)
  5. 5. Halide 4 画像処理用 C++ 埋め込み DSL Func blur_3x3(Func in) { Func bx, by; Var x, y, xi, yi; bx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3; by(x, y) = (bx(x, y-1) + bx(x, y) + bx(x, y+1))/3; by.tile(x, y, xi, yi, 256, 32) .vectorize(xi, 8).parallel(y); bx.compute_at(by, x).vectorize(x, 8); return by; } 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); }}}}}
  6. 6. 特徴 画像処理用に制限されたプリミティブ – 出力画素と入力画素の関係式を記述、明示的なループは不要 – 汎用ループ (while) や if 文は書けない アルゴリズムとスケジューリング (最適化) の分離 – タイリング、並列化、ループ順序の交換等を簡単に指定可能 必要な中間バッファの自動確保 様々なバックエンドのサポート – CPU architectures: X86, ARM, MIPS, Hexagon, PowerPC – Operating systems: Linux, Windows, macOS, Android, iOS, Qualcomm QuRT – GPU Compute APIs: CUDA, OpenCL, OpenGL, OpenGL Compute Shaders, Apple Metal, Microsoft Direct X 12 5
  7. 7. 例 6 Func blur_3x3(Func in) { Func bx, by; Var x, y, xi, yi; bx(x, y) = (in(x-1, y) + in(x, y) + in(x+1, y))/3; by(x, y) = (bx(x, y-1) + bx(x, y) + bx(x, y+1))/3; ... } 関数 変数 関数の定義
  8. 8. 例 7 Func blur_3x3(Func in) { Func bx, by; Var x, y, xi, yi; RVar r(-1, 3); bx(x, y) = sum(r, in(x+r, y)); by(x, y) = sum(r, bx(x, y+r)); ... } 関数 変数 関数の定義 -1~1 を動く変数
  9. 9. Genesis Fixstars で開発している Halide の FPGA 回路用 バックエンド – Vivado HLS で合成可能な C++ プログラムを生成 – 一部のスケジューリングをサポート • unroll: 演算器を増やす アクセスパターンを解析して、可能な限りストリーム化 8
  10. 10. ストリーム処理 9 in: f: // -1 <= r.x, r.y <= 1 f(x, y) = sum(in(x + r.x, y + r.y));
  11. 11. ストリーム処理 10 // -1 <= r.x, r.y <= 1 f(x, y) = sum(in(x + r.x, y + r.y)); +in: f:
  12. 12. ストリーム処理 11 f(x, y) = sum(in(x + r.x, y + r.y)); + : レジスタ :BRAM ラインバッファ:
  13. 13. ストリーム処理 12 f(x, y) = sum(in(x + r.x, y + r.y)); : レジスタ :BRAM
  14. 14. ストリーム処理 13 f(x, y) = sum(in(x + r.x, y + r.y)); : レジスタ :BRAM
  15. 15. ストリーム処理 14 f(x, y) = sum(in(x + r.x, y + r.y)); : レジスタ :BRAM
  16. 16. ストリーム処理 15 f(x, y) = sum(in(x + r.x, y + r.y)); + : レジスタ :BRAM
  17. 17. ざっくりとした Genesis のコンパイルフロー 16 Func による定義 ループネスト (Halide IR) ストリーム処理 (Halide IR) Vivado HLS C++ Halide 本家と共通 for (y, 0, h) for (x, 0, w) sum = 0 for (ry, -1, 3) for (rx, -1, 3) sum += in(x+rx, y+ry) f(x, y) = sum f(x, y) = sum(r, in(x+rx, y+ry)) linebuffer<...> lb; for (y, 0, h) for (x, 0, w) v = in.read(); lb.push(v); sum = 0 for (ry, -1, 3) for (rx, -1, 3) sum += lb(rx, ry) f.write(sum) ストリーム化可能? - 添字式がループ変数の線形式 - ...
  18. 18. ストリーム化 17 for (y, 0, h) for (x, 0, w) sum = 0 for (ry, -1, 3) for (rx, -1, 3) sum += in(x+rx, y+ry) f(x, y) = sum linebuffer<...> lb; for (y, 0, h) for (x, 0, w) v = in.read(); lb.push(v); sum = 0 for (ry, -1, 3) for (rx, -1, 3) sum += in(x+rx, y+ry) f.write(sum) ストリーム (の読み書きを行う) ループ linebuffer<...> lb; for (y, 0, h) for (x, 0, w) v = in.read(); lb.push(v); sum = 0 for (ry, -1, 3) for (rx, -1, 3) sum += lb(rx, ry) f.write(sum) ストリームループの直下に ストリームの読み書きを追加 入力 Func を呼ぶ代わりに ラインバッファ内のレジスタにアクセス (0,0) に (x,y) のデータが入っているので インデックスから(x,y) を引く -1 0 1 -1 x-1, y-1 x, y-1 x+1, y-1 0 x-1, y x y x+1, y 1 x-1, y+1 x, y+1 x+1, y+1
  19. 19. ラインバッファの大きさの推論 ラインバッファのレジスタはどれだけのサイズが必要? – ストリームループの内側でアクセスされる要素分だけ必要 • 前の例だと [-1, 1] x [-1, 1] 必要 変数の動く区間を求めて添字式の 動く範囲を求める できる限り小さい範囲を求めたい →必要なレジスタの個数が減って嬉しい 18 linebuffer<...> lb; for (y, 0, h) for (x, 0, w) v = in.read(); lb.push(v); sum = 0 for (ry, -1, 3) for (rx, -1, 3) sum += lb(rx, ry) f.write(sum)
  20. 20. 解析したい添字式の例 19 x x+r // repeat_edge min(max(x+r, 0), w-1) // mirror_image select(x+rx < 0 || x+rx >= w, select ((x+rx) % 2*w >= w, 2*w - 1 - (x+rx)%2*w, (x+rx)%2*w) min(max(idx, 0), w - 1) A B C D E E E E E A B C D E E D C B // mirror_interior select(x+r < 0 || x + r >= w, w – 1 - abs((x+r) % (2*(w-1)) – (w - 1)) min(max(x+r, 0), w – 1) A B C D E D C B A
  21. 21. 範囲解析 変数とその動く範囲の組の集合 Γ と式 𝑒 が与えられるので、 式の動く範囲RΓ(𝑒)を推論する – 例: Γ = {𝑟𝑥: −1, 1 , 𝑟𝑦: −1, 1 } のとき、RΓ(𝑟𝑥 + 𝑟𝑦) は? 答え: −2, 2 最初のアプローチ: Halide の 範囲解析器を流用する – src/Bounds.cpp に関数 bounds_of_expr_in_scope として 実装されている 20
  22. 22. 区間演算 (Interval arithmetic) 区間同士の演算を定義する – Γ = {𝑥: 0,3 , 𝑦: 1, 4 }のとき、 RΓ 𝑥 + 𝑦 = 0 + 1,3 + 4 = 1,7 RΓ 𝑥 − 𝑦 = 0 − 4,3 − 1 = −4, 2 RΓ 𝑥 ⋅ 𝑦 = min 0 ⋅ 1, 0 ⋅ 4, 3 ⋅ 1, 3 ⋅ 4 , max … = [0, 12] 21
  23. 23. 問題点1: 式の簡約が必要性 例  𝑥 ∈ 0, 𝑤 − 1 , 𝑟 ∈ −1, 1 のとき 𝑥 + 𝑟 − 𝑥 の区間は? 𝑥 + 𝑟 − 𝑥 = 0, 𝑤 − 1 − −1,1 − 0, 𝑤 − 1 = −1, 𝑤 − 0, 𝑤 − 1 = [−𝑤, 𝑤] – −1, 1 が求まってほしいのに… できる限り式を簡約化してやる必要がある 𝑥 + 𝑟 − 𝑥 = 𝑟 = [−1,1] 22
  24. 24. 問題点2: 𝑥 ∈ −10,10 のときselect 𝑥 < 0, −𝑥, 𝑥 の区間は? – Halide の実装: select 𝑏, 𝑥, 𝑦 = 𝑥 ∪ 𝑦 – select 𝑥 < 0, −𝑥, 𝑥 = −10, 10 ∪ −10, 10 = [−10, 10] • 0, 10 がとなってほしい… – 条件式から区間を求めて活用する必要がある • 𝑅Γ(select 𝑏, 𝑥, 𝑦 ) = 𝑅Γ∩𝐼 𝑏 (𝑥) ∪ 𝑅Γ∩𝐼¬ 𝑏 (𝑥) 𝐼 𝑏: 𝑏が真のときに各変数が動く区間 • 𝑅 select 𝑥 < 0, −𝑥, 𝑥 = 𝑅 𝑥: −10,−1 −𝑥 ∪ 𝑅 𝑥: 0,10 𝑥 = [0,10] • 𝐼 𝑏はどうやって求める? 23
  25. 25. 𝑰 𝒃 の求め方 Halide の関数 solve_for_outer_interval(e, x) をつかう – e が真になるような x の区間を求めてくれる • (x<=10,x) = [-∞, 10] – 大量の書き換え規則の組み合わせでできている • 解いてくれるように構文木を変換する必要がある • select(E[select(b, x1, x2)], y1, y2) -> select(b, select(E[x1], y1, y2), select(E[x2], y1, y2)) • アドホックな書き換えをたくさんする必要がある 24 Expr expr = e; debug(1) << "Before normalization: " << expr << 'n'; expr = selectize_clamp(expr); expr = selectize_modulo(expr); expr = selectize_abs(expr); expr = split_select_cond(expr); debug(1) << "After selectize: " << expr << 'n'; expr = simplify_redundant_select(expr); expr = simplify(expr, true, scope); expr = split_select_cond(expr); debug(1) << "After simplification: " << expr << 'n'; expr = hoist_select_in_condition(expr); expr = lift_select_in_binop(expr); debug(1) << "After lifting: " << expr << 'n'; expr = simplify_redundant_select(expr); expr = lift_select_in_binop(expr); expr = simplify_div_mul(expr); expr = lift_select_in_binop(expr); expr = simplify(expr, true, scope); expr = split_select_cond(expr); debug(2) << "Before div mod " << expr << 'n'; debug(2) << "After div mod " << expr << 'n';
  26. 26. 問題点3: 矩形しか表すことができない 0 ≤ 𝑥 ≤ 3, 0 ≤ 𝑦 ≤ 3のときselect(𝑥 + 𝑦 ≤ 3, 𝑥 + 𝑦, 0) の 区間は? – 𝑥 + 𝑟 ≤ 3 ⇔ 𝑥 ≤ 3 − 𝑦 = 0,3 なので 𝐼 𝑥+𝑦≤3 𝑥 = 0,3 , 𝐼 𝑥+𝑦≤3 𝑦 = 0,3 – 𝑥 + 𝑦 = 0,3 + 0,3 = [0,6] 25 x y 3 3
  27. 27. 区間演算の問題点 問題点1: 式を簡約化しなくてはならない – アドホックな構文木変換ルールが大量にできてしまった 問題点2: 𝐼 𝑏 を頑張って求める必要がある – アドホックな構文木変換ルールが大量にできてしまった • select のネストに対して指数オーダーの構文木ができてしまって、 解析に非常に時間がかかる 問題点3: 矩形しか表すことができない – 区間解析の本質的な制限 – レジスタサイズが大きくなってしまう 26
  28. 28. Z3によるウィンドウサイズ推論 ストリーム可能ならば、添字式は (ほぼ) 線形 – 線形ならばいろいろな問題が決定可能 (Presburger 算術) Z3: SMT (Satisfiability Modulo Theory) ソルバ – プログラマ向け説明: bool 型の式を true にする変数の割当を 見つけてくれる • 例: 0 ≤ 𝑥 < 𝑤, −1 ≤ 𝑟 ≤ 1, 0 ≤ 𝑥 + 𝑟 ≤ 1 を満たす𝑥, 𝑟の組は? • select式も使える – 最適化問題も解ける • 制約のもとで最小値と最大値を求めれば区間が求まる! 27
  29. 29. Z3によるウィンドウサイズ推論 1. Halide の式と各区間を Z3 の式に変換する 2. 区間のもとで式の最大値と最小値を求める 簡単! 28 z3::context ctx; z3::expr z3_expr = halide_to_z3(expr, ctx); z3::optimize opt(ctx); for (auto it = bounds.cbegin(); it != bounds.cend(); ++it) { z3::expr constraint = inside_interval_constraint(it.name(), it.value(), ctx); opt.add(constraint); } for (auto c : constraints) { opt.add(halide_to_z3(c, ctx)); } z3::optimize::handle h = opt.maximize(z3_expr); auto check_res = opt.check(); if (check_res == z3::sat) { z3::expr m = opt.lower(h); }
  30. 30. 評価 convolution (𝑥 + 𝑟) に境界条件を加えた式で評価 – 最大18s くらい 29 0 2000 4000 6000 8000 10000 12000 14000 16000 18000 20000 repeat_edge mirror_image mirror_interior 区間を求めるのにかかった時間 (ms) min max
  31. 31. 感想 基本的には速いし正確だがたまによく分からない 動きをする – mirror_image/interior で実行時間が100倍以上違う – Halide のテストケースを流してみたら 非決定的に SEGV – 変数の範囲を変えてみたら解けなくなった (unknown を返す) CVC4という他の SMT ソルバでも試してみたが、 mirror_interior は数時間待っても解けなかった – CVC4 は optimize が無いので二分探索で実装 30
  32. 32. まとめ Genesis のストリーム変換の概要と Z3 を使った レジスタ範囲解析を紹介した 実際のストリーム変換はもっと大変 – いろいろな要素を考慮してコード生成する必要がある • 入出力の帯域、unroll、添字式の係数、etc. Z3 のレジスタ解析はおおむねよく動いている – が、たまによく分からない挙動をする 31

×