Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
5
Help us understand the problem. What is going on with this article?
@SatoshiTerasaki

Chainer V5 で出ている Static Subgraph Optimizations を触りましょ。

More than 1 year has passed since last update.

本日は(も)

カレンダー空いているのでChainerネタ書いてみます。
2,3日目は ChainerX という未来を git clone Chainerのリポジトリ を導入ることでできました。
12月3日に v6.0.0b1 のリリースアナウンスがあったのでリポジトリを落とさなくても

$ pip install --pre chainer

によって未来へタイプスリップできるようになりました。

同時に 5.1.0 のバージョンもできたのでV5で導入された Static Subgraph Optimizations を使ってみようと思います。

概要

によれば

学習を通して変化しない静的グラフをキャッシュすることで、その部分の計算やメモリ使用を最適化し、学習速度を20~60%高速化

とのことです。

Static subgraph optimization feature has been introduced. The CPU (Python) overhead of graph construction and traversal in backward is removed with it.

となっています。計算グラフの構築におけるPythonのオーバヘッドっでそんなに重いんですね・・・。

Advanced graph optimizations/transformations are not implemented yet, so currently it only reduces the CPU overhead. We will consider adding more sophisticated graph-level optimizations to improve the GPU utilization as well as further reduce CPU overhead.

要するにもっと改善の余地はあるようなので、今後のアップデートによって学習の高速化が実現できるかもしれません(わきゅわきゅ)。

の資料を見るといろいろなノウハウで学習速度を速めている様子がわかります。

Install

未来へタイムスリップしたお客様は念の為アンインストールしておきます

$ pip uninstall chainer cupy

そして再度インストールします

$ pip install chainer==5.1.0 
$ pip install cupy-cuda90==5.1.0

単に pip install cupy でもいいですがビルドが走るので cupy-cuda90 ないしは cupy-cuda92 などでもよいでしょう。くわしくはこちら

$ cat /usr/local/cuda/version.txt

で各自のバージョンを確認しておくとよいでしょう。

使い方

もう悩む必要はありません!ドキュメントを眺めればほら簡単(テレビショッピング風)

To enable static graph optimizations, it is only necessary to add the @static_graph decorator to a chain’s call() method. We will now show how the Chainer MNIST example can be modified to use this feature. The modified version with static subgraph optimizations is located at chainer.examples.static_graph_optimizations.mnist.

つまり

import chainer
from chainer import static_graph

class BOKUNO_KANGAETA_SAITUYO_NETWORK(chainer.Chain):
    def __init___(self):
        super(BOKUNO_KANGAETA_SAITUYO_NETWORK,self).__init__():
        with self.init_scope():
            #write some links
    @static_graph
    def __call__(self,x):
        #write something

のようにすれば良いみたいですね。

デコレートについては Pythonのデコレータを理解するための12Step などが参考になります。

試すコード

ChainerのリポジトリのExample にもありますが、ここではChainerを勉強していた時に書いていた古き良き時代のCNNを用いたMNSIT学習コードを改造したものを使ってみます。

cnn_mnist.py
import argparse

import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training, datasets, iterators, optimizers
from chainer.training import extensions
import numpy as np
from tqdm import tqdm

major, _, _ = chainer.__version__.split(".")
MAJOR = int(major)
if MAJOR >= 5:
    from chainer import static_graph
else:
    def static_graph(func):
        """
        dummy decorator to keep compatibility between Chainer v5 and v4
        """

        def wrap(self, *args, **kwargs):
            return func(self, *args, **kwargs)
        return wrap

DEVICE = 0
BATCH_SIZE = 32


class MNISTCONV(chainer.Chain):

    def __init__(self, use_static):
        super(MNISTCONV, self).__init__()
        ksize = 3
        hidden_ch = 8
        self.use_static = use_static
        with self.init_scope():
            self.c1 = L.Convolution2D(1, hidden_ch, ksize=ksize, stride=1, pad=ksize // 2)
            self.c2 = L.Convolution2D(hidden_ch, hidden_ch, ksize=ksize, stride=1, pad=ksize // 2)
            self.c3 = L.Convolution2D(hidden_ch, hidden_ch, ksize=ksize, stride=1, pad=ksize // 2)
            linear_size = 28 // 2
            self.l1 = L.Linear(linear_size * linear_size * hidden_ch, 10)

    def _forward(self, x):
        h = self.c1(x)
        h = self.c2(h)
        h = self.c3(h)
        h = F.relu(h)
        h = F.max_pooling_2d(h, 2)
        h = self.l1(h)
        return h

    @chainer.static_graph
    def static_forward(self, x):
        return self._forward(x)

    def forward(self, x):
        if self.use_static and chainer.backends.cuda.get_array_module(x) != np:
            return self.static_forward(x)
        else:
            return self._forward(x)

    def __call__(self, x, t=None, train=True):
        h = self.forward(x)
        if train:
            loss = F.softmax_cross_entropy(h, t)
            chainer.reporter.report({
                'loss': loss,
            }, self)
            return loss
        else:
            return F.softmax(h)


def train(model):
    if DEVICE >= 0:
        chainer.cuda.get_device_from_id(DEVICE).use()
        chainer.cuda.check_cuda_available()
        model.to_gpu()
    train, test = chainer.datasets.get_mnist(ndim=3)
    train_iter = iterators.SerialIterator(train, BATCH_SIZE, shuffle=True)
    test_iter = iterators.SerialIterator(test, BATCH_SIZE, repeat=False, shuffle=False)

    optimizer = optimizers.SGD()
    optimizer.setup(model)

    updater = training.StandardUpdater(train_iter, optimizer, device=DEVICE)
    trainer = training.Trainer(updater, (5, 'epoch'), out='result')
    trainer.extend(extensions.Evaluator(test_iter, model, device=DEVICE))
    trainer.extend(extensions.ProgressBar())
    trainer.extend(extensions.LogReport())
    trainer.extend(extensions.PrintReport([
        'epoch', 'elapsed_time',
        'main/loss', 'validation/main/loss',
    ]))
    trainer.run()

    chainer.serializers.save_npz('result/mnistconv.npz', model)


def predict(model, use_ideep):
    chainer.serializers.load_npz('result/mnistconv.npz', model)
    train, test = chainer.datasets.get_mnist(ndim=3)
    counter = 0
    acc = 0
    if use_ideep and chainer.backends.intel64.is_ideep_available():
        model.to_intel64()
    with chainer.using_config('use_ideep', 'auto'):
        for t in tqdm(test):
            counter += 1
            x, ans = t
            result = model(np.array([x]), train=False).data[0]
            if ans == np.argmax(result):
                acc += 1
        print(acc / counter)


def parse_arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument('--static', action='store_true', help='decorate static_graph on training')
    parser.add_argument('--ideep', action='store_true', help='use ideep on prediction')
    args = parser.parse_args()
    return args


def main():
    args = parse_arguments()
    klass = MNISTCONV
    use_static = args.static
    use_ideep = args.ideep

    model = klass(use_static)
    train(model)
    # initialize
    model = klass(use_static)
    predict(model, use_ideep)


if __name__ == '__main__':
    main()

このコードの使い方

$ python cnn_mnist.py

でいけます。ただし、この場合は Static Subgraph Optimizations の機能が使われないようになっています。

実行例を下記に示します。

epoch       elapsed_time  main/loss   validation/main/loss
1           13.5793       0.332974    0.149877              
2           25.9988       0.126915    0.0866168             
3           38.7518       0.0898346   0.0763928             
4           51.3052       0.0743761   0.0750592             
5           63.7166       0.0664786   0.0607661             
100%|██████████████████████████████████████████████████████████████████████████████████████| 10000/10000 [00:27<00:00, 361.07it/s]
0.98

Static Subgraph Optimizations を有効にするには --static をつけます:

$ python cnn_mnist.py --static
end of build_schedule()
Creating new backward schedule...
end of build_schedule()
epoch       elapsed_time  main/loss   validation/main/loss
end of build_schedule().......................................] 19.20%
end of build_schedule()#####################################..] 96.00%
1           12.0856       0.334284    0.135471              
2           23.1138       0.118735    0.0932434             
3           34.2854       0.0838322   0.0617251             
4           45.5717       0.0688249   0.054637              
5           56.6426       0.0596717   0.0525059             
100%|██████████████████████████████████████████████████████████████████████████████████████| 10000/10000 [00:25<00:00, 388.59it/s]
0.9823

63秒かかった学習が56秒にへりました。約1.12倍はやくなりました。やったね!

うまくいかないこともある

まだ experimental な機能なのでうまく行かないケースもあります。今回は極めて単純なケースなのでうまく行きましたが、現段階ではいたずらに @static_graph をつけると全てうまく行くというわけではなさそうです。たとえば、今回のサンプルコードで

__call__ にデコレートすると失敗します。

リリースノートにあるようにこれらの機能を使うには forward のコードの書き方をそれを安全に運用できるように書き方に注意しないといけないようです。

By applying @static_graph decorator to functions or methods (typically it is the forward method of a chain), you can let Chainer cache the computational graph collected at the first call and reuse it from the subsequent calls. To use this feature safely, your define-by-run code must always perform the same computations each iteration.

できない例たち

  • ImageNet の Example にあるようなGoogLeNetの __call__ 関数をデコレートしても end of build_schedule() メッセージが大量に出てきてメモリーオーバします。
  • MobileNetV2 で遊んでいると一応デコレートする前はちゃんとロスが収束していたのに、つけるとValidationが学習回しても減らないみたいなのはあります.(どのレイヤーが悪さをしているのかまだ切り分けできていない)
  • 5.0.0 のときは chainer.links.BatchNormalization で use_gamma=False を指定したレイヤーを推論で通すと実行時エラーが起きる(再現コードなくしたのでなんとも言えない)

推論ではつかえるか?

原理的にはYesなはず。自分が書いたコードでGPUを用いた推論のスピードが向上するというケースもありました個人的にはこれだけでも嬉しいです。

ただし、今回の例では(コードを変更して推論時にも有効となるようにすると)うごくっちゃうごくんですが毎回 end of build_schedule() が走るのでうまくできていない様子がわかります。このプリントオプションなんとかしたい。

ideep と併用できる?

少し期待しましたが、だいたい失敗します。上のコードをいじると失敗させることができます。

デコレートすると Chainer V4 でコードが動かない(そりゃそうだ)。

V4以下では機能がないのでそりゃそうなんですが、V5が出たばかりで、V4で学習、推論ができた資産を云々という理由で実行環境として V4 と V5 で両立したいというニーズはあると思います。V5でこの機能をパーフェクトにつかえるシチュエーションがあるのにV4との互換性を意識して実装しない(あきらめる)みたいなのはちょっと悔しいですよね。

回避策(たとえば)

上のサンプルコードでも書いていますが、

major, _, _ = chainer.__version__.split(".")
MAJOR = int(major)
if MAJOR >= 5:
    from chainer import static_graph
else:
    def static_graph(func):
        def wrap(self, *args, **kwargs):
            return func(self, *args, **kwargs)
        return wrap

のようにしてChainerのメジャーバージョンに応じて static_graph の意味を変えるという方法があります。V4ではダミーのデコレータを定義する方法がありますね。

fowardの方も使用条件に応じて static_graph でデコレートされたところを通すか通さないかの条件分岐をつくると回避できます。たとえば、学習時はStatic Subgraph Optimizationsを活用したい。でも(何かしらの理由で)推論時は ideepで高速化させたいのときは

    def _forward(self, x):
        # ここが実質の本体

    @chainer.static_graph
    def static_forward(self, x):
        return self._forward(x)

    def forward(self, x):
        #ただのインターフェース。
        if chainer.backends.cuda.get_array_module(x) != np:
            #学習の時はここを通る
            return self.static_forward(x)
        else:
            # CPUで実行する場合ここを通る
            return self._forward(x)

のようにしておいて呼び出し側は model.forward(np.array([x]).astype(np.float32)) のインターフェースを守るという設計にしておけば良いかなと考えています。

まとめ

  • Static Subgraph Optimizations の機能を触りました。デコレートするだけで簡単に使えます(ここ大事)。
  • それを実感するサンプルコードを書きました。いろいろ変更すると何がダメでなにがうまく行くかを体感することができます。
  • この機能がうまく使えるケースがあればあなたは幸運です。十分有効活用してChainerを布教させてください。
  • V4とV5でコードを両立するテクニックを紹介しました。

おまけ

今回の記事では関係ないですが、
V5 で L.DepthwiseConvolution2D の実装が変わったらしく、GPUでの推論が爆遅になるケースがありますが chainer.config.autotune=True を適用することで改善します。

以上!!!

5
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
SatoshiTerasaki
Julia/Pythonの記事を書いています.

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
5
Help us understand the problem. What is going on with this article?