PythonのPEP 3118 -- Revising the buffer protocol相当の仕組みをRubyでも実現したい。
須藤功平 - 2019-05
大きなデータ(多次元配列や画像データ)を相互に交換するための統一的な仕組みが欲しい。統一的な仕組みがあると実装が単純になるからである。
私はApache Arrow(多次元配列のためのデータ構造がある)やgdk-pixbuf(画像処理ライブラリー)やOpenCV(コンピュータービジョンライブラリー)のRubyバインディングを開発している。これらの各バインディングがなにもかも必要な処理をすべてできるわけではない。たとえば、gdk-pixbufは代表的な高レベルな画像処理(読み書き変換)はできるが、ピクセルレベルの処理を高速に実現することはできない。そのため、必要に応じて対象の処理が得意なライブラリーを活用したい。たとえば、ピクセルレベルの処理は多次元配列ライブラリーを使うことで高速に処理できる。
そのため、私はいくつか相互にデータを交換する補助ライブラリーを作った。たとえば、Apache Arrowの多次元配列データとgdk-pixbufの画像データを相互に変換するred-arrow-gdk-pixbufである。これで各ライブラリーの得意な処理を組み合わせることができる。(今のApache Arrowのテンソルにはピクセルレベルの処理を高速に実現する機能はないので、例として微妙。)
ただ、このように個別のライブラリーごとにデータ交換処理を実現していると組み合わせの数が一気に増えてしまう。たとえば、gdk-pixbufとOpenCV間、Apache ArrowとOpenCV間でのデータ交換処理も実装しなければいけない。(使う必要がなければ別に実装する必要はないので例として微妙。)しかし、統一的な仕組みがあれば、各ライブラリーが統一的な仕組み1つに対応するだけで各種ライブラリー間で相互に交換できる。このためデータ交換の実装が単純になる。
基本的にPEP 3118をベースに考えるのでよさそうだがいくつか改良したい。
-
中身の表現の仕方を↓のように暗号的な感じではなくもっとヒューマンリーダブルな感じにできないか。
- Additions to the struct string-syntax
- Examples of Data-Format Descriptions
- RGBの順番も表現できる表現力はすばらしい。(gdk-pixbufとOpenCVではRGBの順番が違う(ことが多い)ので順番を変えなければいけない。)
-
単語の区切りには
_
を入れたい。ndim
→n_dim
とかn_dimensions
とか。
-
CPU上のメモリーだけではなくGPU上のメモリーも扱えるような仕組みがよさそう。
- FPGAも?(よくわかっていない)
__cuda_array_interface__
が参考になるかも。
The Array Interfaceの先頭に書いているが、PEP 3188は__array_interface__
プロトコルのスーパセットである。せっかく今から整備するならPEP 3188の方がよいのではないか。(せっかくってなんだ?)
NumBufferは__array_interface__
プロトコル相当のもので、数値の多次元配列のための実装である。せっかくなので数値以外もサポートするのはどうだろう。(せっかくってなんだ?)
Apache Arrowは言語中立なデータ交換のためのフォーマットを定義しようとしているのでよさそうである。ただ、数値の多次元配列のための実装である。せっかくなので数値以外もサポートするのはどうだろう。(せっかくってなんだ?)
Apache ArrowにPEP 3188相当の拡張性を提案するのもありかもしれない。
ndtypesでもろもろの型を表現できそう(RGBも含めて)だし、PEP-3118の構文から変換できそうだし悪くないかもしれない。
Pythonでは異なる多次元配列の実装(たとえばNumPyとCuPy)でも同じAPIで動かせる__array_function__
プロトコルというプロトコルも検討している。これも有用そうだが今回は対象外とする。
参考までにどんなことがうれしくなるかというのをざっくりと説明する。
Red ChainerではCPU上で処理するときはNumoを使い、GPU上で処理するときにはCumoを使っている。違う多次元配列の実装を使っていてもできるだけコードの重複をなくし、メンテナンス性を高めるために次のようにxm
という変数(中身はNumo
かCumo
モジュール)を経由して名前解決をしている。
https://github.com/red-data-tools/red-chainer/blob/master/lib/chainer/optimizers/adam.rb#L32-L33
xm = Chainer.get_array_module(grad)
param.data -= lr * @state[:m] / (xm::NMath.sqrt(@state[:v]) + hp.eps)
__array_function__
プロトコル相当のことが実現できると、たとえば次のように書いただけでNumoでもCumoでも動く。
param.data -= lr * @state[:m] / (Numo::NMath.sqrt(@state[:v]) + hp.eps)
微妙だね。どうしようね。私が本当に必要な気になったらがんばるのでいいかもしれない。