.. _sec_hybridize: コンパイラとインタプリタ ======================== これまで本書では、\ ``print``\ 、\ ``+``\ 、\ ``if`` などの文を用いてプログラムの状態を変更する命令型プログラミングに焦点を当ててきた。以下に、単純な命令型プログラムの例を示す。 .. raw:: latex \diilbookstyleinputcell .. code:: python def add(a, b): return a + b def fancy_func(a, b, c, d): e = add(a, b) f = add(c, d) g = add(e, f) return g print(fancy_func(1, 2, 3, 4)) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output 10 Python は *インタプリタ型言語* である。上の ``fancy_func`` 関数を評価するとき、関数本体を構成する操作を *順番に* 実行する。つまり、\ ``e = add(a, b)`` を評価して結果を変数 ``e`` に保存し、それによってプログラムの状態を変更する。続く 2 つの文 ``f = add(c, d)`` と ``g = add(e, f)`` も同様に実行され、加算が行われて結果が変数として保存される。 :numref:`fig_compute_graph` はデータの流れを示している。 .. _fig_compute_graph: .. figure:: ../img/computegraph.svg 命令型プログラムにおけるデータフロー。 命令型プログラミングは便利であるが、非効率な場合がある。ひとつには、\ ``fancy_func`` 全体で ``add`` 関数が繰り返し呼び出されても、Python は 3 回の関数呼び出しを個別に実行するからである。これらが、たとえば GPU 上(あるいは複数 GPU 上)で実行される場合、Python インタプリタに起因するオーバーヘッドが非常に大きくなることがある。さらに、\ ``fancy_func`` 内のすべての文が実行されるまで、変数 ``e`` と ``f`` の値を保存しておく必要がある。これは、文 ``e = add(a, b)`` と ``f = add(c, d)`` が実行された後に、変数 ``e`` と ``f`` がプログラムの他の部分で使われるかどうかが分からないためである。 シンボリックプログラミング -------------------------- 代替案として *シンボリックプログラミング* を考えよう。これは通常、処理が完全に定義されてから計算を行う方式である。この戦略は、Theano や TensorFlow(後者は命令型拡張を取り込んでいます)を含む複数の深層学習フレームワークで使われている。通常、次の手順を含む。 1. 実行する操作を定義する。 2. 操作を実行可能なプログラムにコンパイルする。 3. 必要な入力を与え、コンパイル済みプログラムを呼び出して実行する。 | これにより、大幅な最適化が可能になる。まず、多くの場合 Python インタプリタを省略できるため、単一の Python スレッドが CPU 上で動作しながら複数の高速 GPU を使うような状況で顕著になりうる性能ボトルネックを取り除ける。 | 第二に、コンパイラは上のコードを ``print((1 + 2) + (3 + 4))``\ 、あるいは ``print(10)`` にまで最適化して書き換えられるかもしれない。これは、コンパイラが機械命令に変換する前にコード全体を見渡せるからである。たとえば、ある変数が不要になった時点でメモリを解放したり(あるいは最初から確保しなかったり)できる。また、コード全体を等価な別のコードへ変換することもできる。 | よりよく理解するために、以下の命令型プログラミングのシミュレーション(結局のところ Python です)を見てみよう。 .. raw:: latex \diilbookstyleinputcell .. code:: python def add_(): return ''' def add(a, b): return a + b ''' def fancy_func_(): return ''' def fancy_func(a, b, c, d): e = add(a, b) f = add(c, d) g = add(e, f) return g ''' def evoke_(): return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))' prog = evoke_() print(prog) y = compile(prog, '', 'exec') exec(y) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output def add(a, b): return a + b def fancy_func(a, b, c, d): e = add(a, b) f = add(c, d) g = add(e, f) return g print(fancy_func(1, 2, 3, 4)) 10 命令型(インタプリタ型)プログラミングとシンボリックプログラミングの違いは次のとおりである。 - 命令型プログラミングは簡単である。Python で命令型プログラミングを使う場合、コードの大部分は素直で書きやすいものである。また、命令型プログラミングのコードはデバッグしやすいである。これは、関連する中間変数の値をすべて取得して表示したり、Python に組み込まれたデバッグツールを使ったりしやすいためである。 - シンボリックプログラミングはより効率的で、移植もしやすいである。シンボリックプログラミングでは、コンパイル中にコードを最適化しやすいだけでなく、Python に依存しない形式へプログラムを移植することもできる。これにより、Python 以外の環境でもプログラムを実行でき、Python インタプリタに関連する潜在的な性能問題を回避できる。 ハイブリッドプログラミング -------------------------- 歴史的に、多くの深層学習フレームワークは命令型かシンボリック型のどちらかを選んできた。たとえば、Theano、TensorFlow(前者に触発された)、Keras、CNTK はモデルをシンボリックに記述する。逆に、Chainer と PyTorch は命令型アプローチを採用している。TensorFlow 2.0 と Keras には、後の改訂で命令型モードが追加された。 上で述べたように、PyTorch は命令型プログラミングに基づいており、動的計算グラフを使用する。シンボリックプログラミングの移植性と効率性を活用するため、開発者たちは両方のプログラミングパラダイムの利点を組み合わせられるかどうかを検討した。その結果、ユーザーは純粋な命令型プログラミングで開発とデバッグを行いながら、製品レベルの計算性能やデプロイが必要なときには、ほとんどのプログラムをシンボリックプログラムへ変換して実行できる torchscript が生まれた。 ``Sequential`` クラスのハイブリッド化 ------------------------------------- ハイブリッド化の仕組みを理解する最も簡単な方法は、複数層を持つ深いネットワークを考えることである。従来は、Python インタプリタがすべての層のコードを実行して、CPU や GPU に転送できる命令を生成する必要があった。単一の(高速な)計算デバイスであれば、これは大きな問題にはならない。一方、AWS P3dn.24xlarge インスタンスのような高度な 8 GPU サーバーを使う場合、Python はすべての GPU を忙しく保つのに苦労する。ここでは単一スレッドの Python インタプリタがボトルネックになる。\ ``Sequential`` を ``HybridSequential`` に置き換えることで、コードのかなりの部分についてこの問題にどう対処できるか見てみよう。まず、単純な MLP を定義する。 .. raw:: html
pytorchmxnetjaxtensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python from d2l import torch as d2l import torch from torch import nn # Factory for networks def get_net(): net = nn.Sequential(nn.Linear(512, 256), nn.ReLU(), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, 2)) return net x = torch.randn(size=(1, 512)) net = get_net() net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output tensor([[0.1385, 0.1115]], grad_fn=) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python from d2l import mxnet as d2l from mxnet import np, npx from mxnet.gluon import nn npx.set_np() # Factory for networks def get_net(): net = nn.HybridSequential() net.add(nn.Dense(256, activation='relu'), nn.Dense(128, activation='relu'), nn.Dense(2)) net.initialize() return net x = np.random.normal(size=(1, 512)) net = get_net() net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output [07:00:25] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output array([[ 0.16526175, -0.14005634]]) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python #@save class Benchmark: """For measuring running time.""" def __init__(self, description='Done'): self.description = description def __enter__(self): self.timer = d2l.Timer() return self def __exit__(self, *args): print(f'{self.description}: {self.timer.stop():.4f} sec') .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python from d2l import tensorflow as d2l import tensorflow as tf from tensorflow.keras.layers import Dense # Factory for networks def get_net(): net = tf.keras.Sequential() net.add(Dense(256, input_shape = (512,), activation = "relu")) net.add(Dense(128, activation = "relu")) net.add(Dense(2, activation = "linear")) return net x = tf.random.normal([1,512]) net = get_net() net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output .. raw:: html
.. raw:: html
``torch.jit.script`` 関数を使ってモデルを変換することで、MLP 内の計算をコンパイルして最適化できる。モデルの計算結果は変わらない。 .. raw:: html
pytorchmxnettensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net = torch.jit.script(net) net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output tensor([[0.1385, 0.1115]], grad_fn=) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net.hybridize() net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output array([[ 0.16526175, -0.14005634]]) .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net = tf.function(net) net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output .. raw:: html
.. raw:: html
これは、あまりにも都合がよすぎるように見えるかもしれない。以前と同じコードを書き、単に ``torch.jit.script`` を使ってモデルを変換するだけである。こうするとネットワークは最適化される(性能は後でベンチマークする)。 ハイブリッド化による高速化 ~~~~~~~~~~~~~~~~~~~~~~~~~~ コンパイルによって得られる性能向上を示すために、ハイブリッド化の前後で ``net(x)`` の評価に必要な時間を比較する。まず、この時間を測定するクラスを定義しよう。これは、性能を測定し(そして改善し)ようとする本章全体で役立ちる。 .. raw:: latex \diilbookstyleinputcell .. code:: python #@save class Benchmark: """For measuring running time.""" def __init__(self, description='Done'): self.description = description def __enter__(self): self.timer = d2l.Timer() return self def __exit__(self, *args): print(f'{self.description}: {self.timer.stop():.4f} sec') では、ネットワークを 2 回呼び出してみよう。1 回は torchscript なし、もう 1 回は torchscript ありである。 .. raw:: html
pytorchmxnettensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net = get_net() with Benchmark('Without torchscript'): for i in range(1000): net(x) net = torch.jit.script(net) with Benchmark('With torchscript'): for i in range(1000): net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output Without torchscript: 1.7613 sec With torchscript: 3.0537 sec .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net = get_net() with Benchmark('Without hybridization'): for i in range(1000): net(x) npx.waitall() net.hybridize() with Benchmark('With hybridization'): for i in range(1000): net(x) npx.waitall() .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output Without hybridization: 0.6049 sec With hybridization: 0.5470 sec .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net = get_net() with Benchmark('Eager Mode'): for i in range(1000): net(x) net = tf.function(net) with Benchmark('Graph Mode'): for i in range(1000): net(x) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output Eager Mode: 1.9632 sec Graph Mode: 0.4807 sec .. raw:: html
.. raw:: html
上の結果から分かるように、\ ``nn.Sequential`` のインスタンスを ``torch.jit.script`` 関数でスクリプト化した後は、シンボリックプログラミングの利用によって計算性能が向上する。 シリアライズ ~~~~~~~~~~~~ モデルをコンパイルする利点のひとつは、モデルとそのパラメータをディスクにシリアライズ(保存)できることである。これにより、選択したフロントエンド言語に依存しない形でモデルを保存できる。したがって、学習済みモデルを他のデバイスへデプロイしたり、他のフロントエンドプログラミング言語を簡単に使ったりできる。同時に、コードは命令型プログラミングで達成できるものよりもしばしば高速である。\ ``save`` 関数を見てみよう。 .. raw:: html
pytorchmxnettensorflow
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net.save('my_mlp') !ls -lh my_mlp* .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output -rw-r--r-- 1 ci ci 651K Aug 18 07:11 my_mlp .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net.export('my_mlp') !ls -lh my_mlp* .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output -rw-r--r-- 1 ci ci 643K Aug 18 07:00 my_mlp-0000.params -rw-r--r-- 1 ci ci 3.2K Aug 18 07:00 my_mlp-symbol.json .. raw:: html
.. raw:: html
.. raw:: latex \diilbookstyleinputcell .. code:: python net = get_net() tf.saved_model.save(net, 'my_mlp') !ls -lh my_mlp* .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output INFO:tensorflow:Assets written to: my_mlp/assets total 72K drwxr-xr-x 2 ci ci 6 Aug 18 07:37 assets -rw-r--r-- 1 ci ci 55 Aug 18 07:37 fingerprint.pb -rw-r--r-- 1 ci ci 68K Aug 18 07:37 saved_model.pb drwxr-xr-x 2 ci ci 66 Aug 18 07:37 variables .. raw:: html
.. raw:: html
まとめ ------ - 命令型プログラミングでは、制御フローを使え、Python の豊富なソフトウェアエコシステムを活用できるため、新しいモデルを設計しやすいである。 - シンボリックプログラミングでは、実行前にプログラムを指定してコンパイルする必要がある。その利点は性能向上である。 演習 ---- 1. 以前の章で興味を持ったモデルを見直しよ。再実装することで計算性能を改善できるか。