Ruby 2.6 の新機能の endless range と Range#% を Numo::NArray と Cumo で対応しました。

自分が参加している Red Data Toolsred-data-tools/ja - Gitter@mrkn さんから、

ArithmeticSequence と Range から begin, end, step, exclude_end を取り出す C API を trunk に追加しました。 ruby/ruby@914a290 だれか、これを使って NArray などの slicing に対応するプルリクエストを作ってみませんか?

という提案を頂いたので、 Numo::NArrayCumo (※ Numo::NArray のGPU 版)で 上記 C API を使用して Ruby 2.6 の新機能の endless rangeRange#% に対応を実施しました。

endless range とは、下記のように 末端の "-1" 指摘を省略できる書き方です。
また、step の alias として % を使用できると、より直感的に書く事ができます。

> a = [0, 1, 2, 3, 4]
 => [0, 1, 2, 3, 4] 
> a[1..-1]
 => [1, 2, 3, 4] 
> a[1...-1]
 => [1, 2, 3] 
> (1..4).to_a
 => [1, 2, 3, 4] 
> (1..4).step(2).to_a
 => [1, 3] 
  • Ruby 2.6 (endless range, Range#%)
> a = [0, 1, 2, 3, 4]
 => [0, 1, 2, 3, 4] 
> a[1..]
 => [1, 2, 3, 4] 
> a[1...]
 => [1, 2, 3, 4] 
> (1..4).to_a
 => [1, 2, 3, 4] 
> ((1..4) % 2).to_a
 => [1, 3] 

このように便利な記法が Numo::NArray や Cumo でも使えると嬉しいので、Ruby 2.6 の環境で同様に書けるように対応しました。

  • Numo::NArray
> require 'numo/narray'
> a = Numo::Int32.new(5).seq
 => Numo::Int32#shape=[5]
[0, 1, 2, 3, 4]
> a[1..]
 => Numo::Int32(view)#shape=[4]
[1, 2, 3, 4] 
> a[1...]
 => Numo::Int32(view)#shape=[4]
[1, 2, 3, 4] 
> a[(1..) % 2]
 => Numo::Int32(view)#shape=[2]
[1, 3] 
  • Cumo
> require 'cumo'
> a = Cumo::Int32.new(5).seq
=> Cumo::Int32#shape=[5]
[0, 1, 2, 3, 4]
> a[1..]
=> Cumo::Int32(view)#shape=[4]
[1, 2, 3, 4]
> a[1...]
=> Cumo::Int32(view)#shape=[4]
[1, 2, 3, 4]
> a[(1..) % 2]
=> Cumo::Int32(view)#shape=[2]
[1, 3]

numo-narray の endless range、Range#%の PR は master に 取り込まれている ので次のリリース(0.9.1.5以降?)で使用可能になると思われます。
cumo は リリース版ですでに 取り込まれている ので 0.2.5 以降で使用可能です。

Ruby 本体側でデータサイエンス向けの機能が取り込まれていくと、データサイエンス用ライブラリ側も便利になっていきますので嬉しいですよね。

Redmineのpdf出力機能で使われているRBPDF gem ライブラリについて

(この記事は Redmine Advent Calendar 2018 - Adventar の13日目の記事です。)

Redmine の PDF 出力機能で使われている 私が作成した RBPDF gem ライブラリについてのお話です。

github.com

RBPDF は下記のような特徴があります。

実際にどのような事ができるかは Examples をご覧下さい。

なお、RBPDFは一から作成したものではなくPHPのPDF出力ライブラリTCPDFを、Redmineで使用するために Ruby に移植したものになります。

Redmine の PDF出力機能の歴史

RBPDFを作成することになった経緯を Redmine の PDF出力機能の歴史と合わせてお話しします。

Redmine の PDF出力機能は 1.0 の頃は FPDF という PHPのライブラリを Ruby に移植した RFPDF が使われていました。 このころは日本語が文字化けするなど多くの問題を抱えていましたが、Defect #61: Broken character encoding in pdf export - Redmine のチケットで edwinmoss さんが開発された 同じくPHP由来のTCPDFベースの RFPDF に置き換え & 修正作業を行うことで無事、Redmine 1.2 で CJK(日本・韓国・中国語)と多くの言語の文字化けが解決しました。

github.com

なお、これが私のRedmine での初の採用パッチになります。 詳細は、第2回勉強会 - redmine.tokyo (当時は shinagawa.redmine)で LTをさせて頂きました。

naitoh.hatenablog.com

RFPDF(TCPDFベース)の機能で画像出力や(Textileから変換された)HTMLの変換などにも対応可能な事がわかったので、Redmine 1.3 で Textileによる書式設定 Feature #69: Textilized description in PDF - Redmine や画像出力 Feature #3261: support attachment images in PDF export - Redmine の対応を実施しました。

ただ、この対応時に多くの課題が見えてきたので、抜本的に解決を図るため edwinmoss さんから RFPDF の開発を引き継ぎ、ベースのTCPDFのバージョンを5.2まで上げる形でPHPからRubyへ移植を実施、RBPDF 1.18 として gem を作成しました。 これは Redmine 2.6 の PDFエクスポートの改善として取り込まれました。

TCPDFをベースとしてgem パッケージを開発した理由は下記になります。

  • Redmine コミッターの @marutosijp さんから、PDF出力機能は複雑なので Redmine 本体から分離したいと伺っていた。
  • Redmine にパッチが採用されるためにはWindows/Linux OS環境で動作しなければならない。
    • Redmine のインストールは Rails アプリを配布する形式であるため Pure Ruby であればインストール(環境構築)のハードルを上げないため、採用される可能性がある。
    • Ruby で HTML対応の PDF出力ライブラリで実用的なものは PDFKit ベースのライブラリがいくつかあったが WebKit に依存しているため採用されそうになかった。
  • Redmine のライセンス(GPL2)と互換のあるLGPL 2.1ライセンスのTCPDF 5.2(PHP)をRubyに移植できれば、RTL言語対応や埋め込みフォント対応、HTMLサポートの改善などが見込める。
    • RedminePHP に移植した CandyCane があるなら、その逆も可能と思った。

詳細は、LTthon | RubyHiroba 2014で発表させて頂きました。

www.slideshare.net

その後、RBPDF 1.19 で埋め込みサブセットフォントをサポートし、Defect #19017: Wiki PDF Export: <pre> not rendered with monospaced font - RedmineRedmine 3.2 で取り込まれCJK 以外の言語の PDFファイルで発生していた(対象言語の全フォント埋め込みによる)肥大化問題が解決されました。 これで自分の当初のゴールとしていた内容の対応は完了しました。

Redmine の PDF出力機能で改善されるといいなと思っている点

PDF出力機能の基盤部分の改善は実施しましたが、使い勝手の部分では時間が取れず手が回っていないのですが、今後、下記が改善されるといいなと思いっています。

以上です。

#RubyData_tokyo Meetupで「Usability of Numo::NArray in Numerical Computing of Ruby.」というタイトルで発表してきました

speee.connpass.com

上記のRubyData Tokyo Meetup で「Usability of Numo::NArray in Numerical Computing of Ruby.」というタイトルで発表してきました 発表資料は下記になります。

www.slideshare.net

Red Chainer の開発中に気づいた Numo::NArray のわかりにくかった所や、Numo::NArray に対して自分が取り組んだ、Inplace/Broadcast性能改善SIMD演算対応dot(transpose)性能改善 などを踏まえ、現在の計算速度の改善状況などをお話しました。

自分でも驚きだったのが、ベンチマークの結果、 加算と減算 (Inplace計算は除く)は Numo::Narrayの方が numpy より計算が速い という事ですね。 資料中のベンチマークのコードと結果は下記にあります。

Numo::NArray vs numpy performance. (CentOS 7(x64) Ruby 2.5.3 Numo::NArray 0.9.1.3, Python 3.6.5 numpy 1.15.4) · GitHub

Deep Learning From Scratch 部分の測定内容(p26,p27)は資料中のURLを参照ください。

Numo::NArray の使い方で自分が理解できていなかったところが、今回の発表で整理できてスッキリしたので Red Chainer の開発に生かしていきたいと思います。

CentOS7(x86_64) で Windows版 numo-narray (x86-mingw32/x64-mingw32)を build する場合の手順

numo-narray の Windows 版は linux でクロスコンパイルし、そのまま配布する fat gem の作成が可能なのですが、 ただ、その方法でハマったので対処方法のメモです。

具体的には numo-narray の Windows 向けビルドで rake-compiler.gem, rake-compiler-dock.gem を使うようになっているのですが、 docker コマンドの操作に root権限が必要なため、sudo 経由で rbenv を実行しないと docker コマンドの操作でdocker グループにユーザーを追加しておかないと、docker のステータスを認識できずに処理が止まります。

rake-compiler/rake-compiler-dock の詳細は下記参照。

主な作業

  • ruby 2.1 以上を rbenv でインストール (手順は割愛)
    • ここでハマったのですが、自分はホームディレクトリに rbenv を入れていたため sudo 実行時 rbenv コマンドを認識できませんでした。
    • 今回のケースはシステム側に rbenv をインストールした方がいいと思います。(未確認)
  • sudo の secure_path 変更

    $ sudo visudo

    • 修正前 Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
    • 修正後 Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/rbenv/bin:/usr/local/rbenv/shims:/home/ユーザー名/.rbenv/bin:/home/ユーザー名/.rbenv/shims
      • /home/ユーザー名/.rbenv/bin:/home/ユーザー名/.rbenv/shims の部分は、gem env コマンドで SHELL PATHを確認。
      • システム側に rbenv をインストールした場合は /home/ユーザー名/.rbenv/bin:/home/ユーザー名/.rbenv/shims 部分は不要なはず。
  • Docker インストール & グループ追加 & 起動 & 一旦ログアウトして反映。

$ sudo yum install docker
$ sudo groupadd docker
$ sudo gpasswd -a $USER docker
$ sudo systemctl start docker
$ exit
  • Docker のサービス確認。
$ sudo systemctl status docker.service 

Docker のサービスがactive なのを確認する。

  • Docker コマンド動作確認。
$ docker ps

sudo なしに実行してエラーしないことを確認。

  • gem インストール
$ gem install rake-compiler
$ gem install rake-compiler-dock
  • numo-narray を git clone
$ git clone https://github.com/ruby-numo/numo-narray.git
$ cd numo-narray

sudo 経由で rake コマンドが動作することを確認。

  • Windowsビルド 管理者権限でビルドを行います。(ここでハマっていた。) docker グループに入って入れば管理者権限は不要です。
$ rake build:windows

ビルドが完了し、pkg 配下に numo-narray-x.x.x.x-x86-mingw32.gem, numo-narray-x.x.x.x-x64-mingw32.gem が作られたら成功。

追記

先ほど、@sonots さんから、docker グループに追加すべきとコメント頂き、sudo は不要でしたので記事を修正しました。手順がスッキリしました。ありがとうございます。

numo-narray と benchmark_driver.gem

@watson1978 さんの blog で Mac 環境だと Linux 環境に比べて numo-narray が遅いというお話があったので調べてみました。

watson.hatenablog.com

今回の比較では、先日の Rubyアソシエーション開発助成成果報告会 で発表されていた

www.ruby.or.jp

@k0kubun さんの benchmark_driver.gem を使いました。

このベンチマークは、複数の Ruby バージョン間の比較や、同一 gem で複数のバージョン比較ができるという優れものです。

使い方は $ gem install benchmark_driver でインストールするだけです。
(rbenv を使用している場合、$ rbenv rehash で benchmark-driver コマンドを使用可能にする必要があります。)
グラフ画像を出力する場合は $ gem install benchmark_driver-output-gruff で追加の plugin をインストールします。 (要 RMagick)

確認環境

ベンチマーク内容

@watson1978 さんのベンチマーク結果をベースに inplace 演算と ブロードキャスト演算を加えて比較してみました。

$ cat numo_N_fp.yaml
contexts:
  - gems: { numo-narray: 0.9.1.2 }
    require: false
    prelude: require 'numo/narray'
  - gems: { numo-narray: 0.9.9.0 }
    require: false
    prelude: require 'numo/narray'

loop_count: 10000
prelude: |
  N = 100000
  xd = Numo::DFloat.new(N).seq
  yd = Numo::DFloat.new(N).seq
  xs = Numo::SFloat.new(N).seq
  ys = Numo::SFloat.new(N).seq
benchmark:
  '[100000] + [100000] (fp64)'        : xd         + yd
  '[100000] + [100000] (fp32)'        : xs         + ys
  '[100000].inplace + [100000] (fp64)': xd.inplace + yd
  '[100000].inplace + [100000] (fp32)': xs.inplace + ys
  '[100000] + 1 (fp64)'               : xd         + 1
  '[100000] + 1 (fp32)'               : xs         + 1
  '[100000].inplace + 1 (fp64)'       : xd.inplace + 1
  '[100000].inplace + 1 (fp32)'       : xs.inplace + 1
  • inplace 演算は指定した変数のメモリ領域を書き換える形で演算するため、結果的に省メモリになります。0.9.9.0(省メモリによる高速化版)で省メモリ化しています。
  • ブロードキャスト演算は指定した変数を全要素に対して同じ計算(上記の場合は+1)をします。0.9.9.0(省メモリによる高速化版)で省メモリ化しています。

上記を下記のように実行します。

  • $ benchmark-driver numo_N_fp.yaml --output markdown (Markdown 出力)
  • $ benchmark-driver numo_N_fp.yaml --output gruff (PNG出力)

CentOS / macOS の結果 (グラフ)

f:id:ju-na:20180711194922p:plainf:id:ju-na:20180711195156p:plain
CentOS (左) / macOS (右) の結果

  • 1秒間あたりの処理数 (i/s)なので、数値が大きい方が速いことを意味します。

  • 非inplace の場合、Linux と比較してMac は 3〜4倍遅い

  • 通常演算(非inplace & 非broadcast)から inplaceやbroadcastに変更した場合、Linuxは1.5〜2倍速くなりますが、Macは4〜11倍速くなりLinuxより速い
  • サンプルソースを作成してアセンブラ出力を確認したところ、Macコンパイラ(Clang)が自動ベクトル(SIMD)化している。
    • Linux の場合、DFloat → SFloat or inplace & broadcast 改善版(0.9.9.0) で、省メモリになりキャッシュに乗ることで高速化。
    • Mac の場合、inplace & broadcast 改善版(0.9.9.0) で、省メモリになりキャッシュに乗りSIMDが効果を発揮することで劇的に高速化。
      • SIMDでは変数をまとめて演算するため、DFloat(8Byte)×2個=16Byte → SFloat(4Byte)×4個=16Byte となりメモリサイズは同一。(省メモリにはならない。)

CentOS 7 の結果 (数値) (※グラフ化とは別に再測定実施。)

numo-narray 0.9.1.2 numo-narray 0.9.9.0
[100000] + [100000] (fp64) 7.150k 7.040k
[100000] + [100000] (fp32) 12.768k 12.692k
[100000].inplace + [100000] (fp64) 12.161k 11.572k
[100000].inplace + [100000] (fp32) 17.069k 22.032k
[100000] + 1 (fp64) 7.936k 8.045k
[100000] + 1 (fp32) 9.273k 12.697k
[100000].inplace + 1 (fp64) 11.570k 14.160k
[100000].inplace + 1 (fp32) 12.875k 27.566k

macOS Sierra の結果 (数値) (※グラフ化とは別に再測定実施。)

numo-narray 0.9.1.2 numo-narray 0.9.9.0 (master)
[100000] + [100000] (fp64) 1.897k 1.829k
[100000] + [100000] (fp32) 3.867k 3.753k
[100000].inplace + [100000] (fp64) 12.431k 15.041k
[100000].inplace + [100000] (fp32) 17.005k 29.455k
[100000] + 1 (fp64) 2.093k 2.126k
[100000] + 1 (fp32) 3.664k 4.101k
[100000].inplace + 1 (fp64) 13.843k 23.250k
[100000].inplace + 1 (fp32) 14.338k 44.210k

以上より、

  • Numo::NArrayが Mac で遅いケースがあるが自動ベクトル(SIMD)化とキャッシュの乗り方に起因する現象のように思われる。
  • 可能なら inplace を使いましょう。

というところでしょうか。

四則演算のベンチマーク (おまけ)

ついでに、RubyKaigi 2018 LTベンチマーク結果を、非 inplace のケースも含めてbenchmark_driver.gemベンチマーク化してみました。

ベンチマーク内容

$ cat broadcast_fp32.yaml
contexts:
  - gems: { numo-narray: 0.9.1.2 }
    require: false
    prelude: require 'numo/narray'
  - gems: { numo-narray: 0.9.9.0 }
    require: false
    prelude: require 'numo/narray'

loop_count: 5000
prelude: |
  x = Numo::SFloat.ones([1000,784])
  y = Numo::SFloat.ones([1000,784])
  z = Numo::SFloat.ones([1000,1])
benchmark:
  '[1000,784]         + [1000,784]': x         + y
  '[1000,784].inplace + [1000,784]': x.inplace + y
  '[1000,784]         + [1000,1]  ': x         + z
  '[1000,784].inplace + [1000,1]  ': x.inplace + z
  '[1000,784]         + 1         ': x         + 1
  '[1000,784].inplace + 1         ': x.inplace + 1
  '[1000,784]         - [1000,784]': x         - y
  '[1000,784].inplace - [1000,784]': x.inplace - y
  '[1000,784]         - [1000,1]  ': x         - z
  '[1000,784].inplace - [1000,1]  ': x.inplace - z
  '[1000,784]         - 1         ': x         - 1
  '[1000,784].inplace - 1         ': x.inplace - 1
  '[1000,784]         * [1000,784]': x         * y
  '[1000,784].inplace * [1000,784]': x.inplace * y
  '[1000,784]         * [1000,1]  ': x         * z
  '[1000,784].inplace * [1000,1]  ': x.inplace * z
  '[1000,784]         * 1         ': x         * 1
  '[1000,784].inplace * 1         ': x.inplace * 1
  '[1000,784]         / [1000,784]': x         / y
  '[1000,784].inplace / [1000,784]': x.inplace / y
  '[1000,784]         / [1000,1]  ': x         / z
  '[1000,784].inplace / [1000,1]  ': x.inplace / z
  '[1000,784]         / 1         ': x         / 1
  '[1000,784].inplace / 1         ': x.inplace / 1

確認結果

f:id:ju-na:20180711210539p:plainf:id:ju-na:20180711210551p:plain
CentOS (左) / macOS (右) の四則演算結果

0.9.9.0(inplace 演算と ブロードキャスト演算時の省メモリによる高速化対応版)の改善効果がよくわかります。 同一gem で複数のバージョン比較ができる benchmark_driver.gem は便利ですね。

#RubyKaigi 2018 RubyData Workshop LTで「Red Data Tools -Red Chainer-」というタイトルで発表しました。

RubyKaigi 2018 の 二日目の RubyData Workshop (2) Red Data Tools Lightning Talks - RubyKaigi 2018で「Red Data Tools -Red Chainer-」というタイトルで、自分が現在参加しているRed Data Tools の生活発表を行いました。

資料はこちらになります。

最初になぜ、Red Data Tools プロジェクトに参加して Red Chainer の開発をしているのかの経緯を説明しました。

f:id:ju-na:20180602172750j:plain

次に Red Data Tools と Red Chainer の概要と、自分の取り組み内容を説明しました。

f:id:ju-na:20180602172758j:plain

具体的に私が何の機能を開発したのか、Red Chainer のリリース機能と合わせて紹介しました。

f:id:ju-na:20180602172810j:plain

また、Red Chainer の開発で Numo::NArray にも改善できる点があることに気づいたので、 Numo::NArray に対して取り組んだ内容を紹介しました。

f:id:ju-na:20180602172821j:plain

※ RubyKaigi 2018 LT でお話しした内容は下記をご覧ください。

naitoh.hatenablog.com

Red Chainer の現在の実装状況を、移植元の Chainer 2.0 の機能と比較することで、どの機能が使えてどの機能の実装が足りないかを説明しました。 まだまだ未実装の部分が多いので、未実装だけど使いたい機能があれば、その部分から開発に参加してみるのはどうでしょうか。

f:id:ju-na:20180602172830j:plain

移植元の Chainer はどんどんバージョンアップしているので、機能差が開いて追いつけない状況なので、開発に参加頂ける方を募集中です。

f:id:ju-na:20180602172842j:plain

最後に

Ruby での DATA処理、またはその開発に興味を持って頂ける人が増えることを期待しています。

#RubyKaigi 2018 LTで「Improve Red Chainer and Numo::NArray performance」というタイトルで発表しました。

naitoh.hatenablog.com で記載した通り、RubyKaigi 2018 LT で発表させて頂きました。

内容は、今自分が py2rb.py を使用して取り組んでいる Red Chainer のポーティング作業中に気づいた、 Red ChainerNumo::NArray に対する処理速度改善のための取り組みです。

具体的には、下記になります。

  • Red Chainer の行列計算(Numo::NArray)の精度を DFloat→SFloat に変更することで mnist 1epoc の学習処理を 1.73倍速くした。
  • Numo::NArrayの inplace と BroadCast 計算の処理速度を最大3.8倍速くした。
  • 残りの下記2点のボトルネックの修正が完了すれば、Red Chainer は python Chainer 2.0 と比較してmnist 1epoc の学習処理が 16%遅いところまで処理速度が改善されること。 (CPU比較)
    • 「転置行列に対する dot 計算 "x.dot(w.transpose)"が遅い」=> view に対する dot が遅いので、dup で配列のコピーを作る"x.dot(w.dup.transpose)"ことで暫定対処可能。
    • 平方根"Numo::NMath.sqrt "が遅い」=> SIMD演算を使用することで対処可能 (暫定実装で確認済)。

最後、時間切れでお伝えしきれませんでしたが、Red Chainer の処理速度の改善が進んできているので、興味がある方はぜひ Red Chainerをさわってみて、RubyDeep Learning を始めてみるきっかけになればと考えています。

なお、OpenBlas を用いた Red Chainer の環境は下記のようにして構築しました。(CentOS7 x86_64環境) ご参考までに。

  • numpy を使って、OSにインストールされている OpenBlas のライブラリパスを確認。
$ python
Python 2.7.5 (default, Aug  4 2017, 00:39:18) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy.distutils.system_info as sysinfo
>>> sysinfo.get_info('atlas')
/usr/lib64/python2.7/site-packages/numpy/distutils/system_info.py:624: UserWarning: 
*********************************************************************
    Could not find lapack library within the ATLAS installation.
*********************************************************************

  self.calc_info()
{'libraries': ['tatlas'], 'library_dirs': ['/usr/lib64/atlas'], 'define_macros': [('ATLAS_WITHOUT_LAPACK', None)], 'language': 'c', 'include_dirs': ['/usr/include']}
>>> sysinfo.get_info('openblas')
{'libraries': ['openblas', 'openblas'], 'library_dirs': ['/usr/lib64'], 'define_macros': [('HAVE_CBLAS', None)], 'language': 'c'}
>>> sysinfo.get_info('lapack')
{'libraries': ['lapack', 'lapack'], 'library_dirs': ['/usr/lib64'], 'language': 'f77'}
>>> sysinfo.get_info('mkl')
{}
>>> 
  • OpenBlas が入っていなければ、インストールしてください。rpm コマンでも OpenBlas のパスがわかります。
# yum install openblas-devel.x86_64
# rpm -ql openblas-devel.x86_64
 (略)
/usr/lib64/libopenblas.so
  • numo-narray と numo-linalg をインストール (nomo-linalg のインストール時に、上記で確認した OpenBlas のパスを指定。)
$ rvm 2.5.1
$ gem install numo-narray
$ gem install numo-linalg -- --with-openblas-dir=/usr/lib64/  --with-atlas-dir=/usr/lib64/   --with-lapack-lib=/usr/lib64/
$ ruby  -r numo/linalg -e "p Numo::Linalg::Loader.libs"
["libopenblaso.so", "liblapacke.so"]
  • red-chainer をインストール、git clone、minist 実行。
$ gem install red-chainer
$ git clone https://github.com/red-data-tools/red-chainer.git
$ cd red-chainer
$ ruby  -r numo/linalg/use/openblas examples/mnist.rb --epoch 1
epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           0.193477                          0.94225                                  28.5615       

※ Red Chainer に転地行列のdotのdupと、Numo::NArrayに平方根に対するSIMD処理の実装(あと、inplace の最適化)を反映して計測した mnist の結果です。