Path: blob/master/site/ja/guide/data_performance.ipynb
25115 views
Copyright 2019 The TensorFlow Authors.
tf.data API によるパフォーマンスの改善
概要
GPU と TPU は、単一のトレーニングステップを実行するために必要な時間を劇的に短縮することができます。ピークパフォーマンスの達成には、現在のステップが終了する前に、次のステップのデータを配信する有効な入力パイプラインが必要となります。柔軟で効率的な入力パイプラインの構築に役立つのが、tf.data
API です。このドキュメントでは、tf.data
API を使用して非常に性能の高い TensorFlow 入力パイプラインを構築する方法を説明します。
読み進める前に、「TensorFlow 入力パイプラインの構築」ガイドに目を通し、tf.data
API の使用方法を学習してください。
リソース
tf.data.Dataset
API
セットアップ
このガイドでは、データセットをイテレートし、パフォーマンスを測定します。次のようなさまざまな要因の影響により、再現可能なパフォーマンスベンチマークを作成することが困難となる場合があります。
現在の CPU 負荷
ネットワークトラフィック
キャッシュなどの複雑なメガニズム
再現可能なベンチマークを提供するために、人工的な例を構築します。
データセット
まずは、ArtificialDataset
という、tf.data.Dataset
から継承するクラスを定義します。このデータセットは次のことを行います。
num_samples
サンプルを生成する(デフォルトは 3)ファイルを開くアクションをシミュレーションするために、最初のアイテムの前にしばらくスリープする
ファイルからデータを読み込む操作をシミュレーションするために、各アイテムを生成する前にしばらくスリープする
このデータセットは tf.data.Dataset.range
に似ており、各サンプルの開始とサンプル間に一定の遅延を追加します。
トレーニングループ
次に、データセットのイテレートにどれくらいの時間がかかるかを測定するダミーのトレーニングループを記述します。トレーニング時間がシミュレーションされます。
パフォーマンスの最適化
パフォーマンスをどのように最適化できるかを示すために、ArtificialDataset
のパフォーマンスを改善します。
単純なアプローチ
コツを使わずに、単純なパイプラインから始め、ありのままのデータセットをイテレートします。
内部的には、次のように実行時間が使われています。
プロットに、トレーニングステップの実行には、次のアクションが伴うことが示されます。
ファイルが開いていない場合は、ファイルを開く
ファイルからデータをフェッチする
トレーニングにデータを使用する
ところが、このように単純な同期実装では、パイプラインがデータをフェッチしている間、モデルはアイドル状態となります。その反対に、モデルがトレーニング中である場合、入力パイプラインがアイドル状態となります。したがって、トレーニングのステップ時間は、開いて、読み取り、トレーニングする時間の和であるということになります。
次のセクションでは、この入力パイプラインに基づいて構築し、性能の高い TensorFlow 入力パイプライン設計のベストプラクティスを説明します。
プリフェッチ
プリフェッチは、トレーニングステップの事前処理とモデルの実行に重なって行われます。モデルがトレーニングステップ s
を実行する間、入力パイプラインはステップ s+1
のデータを読み取っています。そうすることで、ステップ時間をトレーニングと、データの抽出にかかる時間の最大時間(和とは反対に)に減少させることができます。
tf.data
API は、tf.data.Dataset.prefetch
変換を提供します。データが生成された時間をデータが消費された時間から切り離すために使用できます。具体的には、この変換は、バックグラウンドのスレッドと内部バッファを使用して、要求される前に入力データセットから要素をプリフェッチします。プリフェッチする要素の数は、単一のトレーニングステップによって消費されるバッチの数と同等(またはそれ以上)である必要があります。この値を手動で調整するか、tf.data.AUTOTUNE
に設定することができますが、後者の場合、tf.data
ランタイムによって、ランタイム時に動的に値が調整されます。
プリフェッチ変換は、「プロデューサ」の作業と「コンシューマ」の作業をオーバーラップする機会があればいつでもオーバーラップさせることに注意してください。
次に、サンプル 0 でトレーニングセットアップが実行している間、入力パイプラインはサンプル 1 のデータを読み取っているのがわかります。
データ抽出の並列化
実世界の状況では、入力データはリモート(Google Cloud Storage や HDFS など)に保管されていることがあります。ローカルとリモートのストレージには、次のような違いがあるため、ローカルでのデータ読み取りに適したデータセットパイプラインは、リモートで読み取られる際にボトルネックとなる可能性があります。
最初のバイトまでの時間: リモートストレージからファイルの最初のバイトを読み取る場合、ロカールストレージからよりもずっと長い時間がかかります。
読み取りのスループット: リモートストレージの総帯域幅は一般的に大きいため、単一のファイルの読み取りには、この帯域幅のほんのわずかしか使用されません。
さらに、生のバイトがメモリに読み込まれると、データのデシリアライズや復号化する必要も出てくるため(protobuf など)、さらに計算が必要となります。このオーバーヘッドは、データの格納場所がローカルであるかリモートであるかに関係なく存在しますが、データのプリフェッチが効果的に行われない場合、リモートの場合に大きくなることがあります。
データ抽出にまつわるさまざまなオーバーヘッドの影響を緩和するために、tf.data.Dataset.interleave
変換を使用して、データの読み込みステップをほかのデータセットのコンテンツ(データファイルリーダーなど)とインターリーブしながら並列化することができます。オーバーラップするデータセットの数は、cycle_length
引数で指定し、並列化のレベルは num_parallel_calls
引数で指定することができます。prefetch
変換と同様に、interleave
変換も tf.data.AUTOTUNE
をサポートしているため、どのレベルの並列化を使用するかという判断は tf.data
ランタイムに委ねられます。
順次インターリーブ
tf.data.Dataset.interleave
変換のデフォルトの引数によって、2 つのデータセットからの単一のサンプルが順次、インターリブされます。
この図は、interleave
変換の動作を示しており、利用できる 2 つのデータセットからサンプルが交互にフェッチされています。ただし、ここでは、パフォーマンスの改善は認められません。
並列インターリーブ
では、interleave
変換の num_parallel_calls
引数を使用してみましょう。これは、複数のデータセットを並列して読み込むため、ファイルが開かれるまでの待機時間が短縮されます。
今度は、データ実行時間のプロットからわかるように、2 つのデータセットの読み取りが並列化され、総合的なデータ処理時間が短縮されています。
データ変換の並列化
データを準備する際、入力要素を事前処理する必要がある場合があります。この目的により、tf.data
API は、ユーザー定義関数を入力データセットの各要素に適用する tf.data.Dataset.map
変換を提供しています。入力要素は互いに独立しているため、複数の CPU コアで事前処理を並列化することができます。これを行うために、prefetch
と interleave
変換と同様に、map
変換でも num_parallel_calls
引数によって並列化のレベルを指定することができます。
num_parallel_calls
引数に最適な値を選択するには、ハードウェア、トレーニングデータの特性(サイズや形状など)、マップ関数のコスト、および CPU で同時に発生しているほかの処理を考慮する必要があります。簡単な調べ方は、利用可能な CPU コアの数を使用することですが、prefetch
と interleave
変換に関して言えば、map
変換は tf.data.AUTOTUNE
をサポートしているため、どのレベルの並列化を使用するかという判断は tf.data
ランタイムに委ねられています。
順次マッピング
基本の例として、並列化を使用せずに map
変換を使用することから始めてみましょう。
単純なアプローチについて言えば、ステップを開いて読み取り、事前処理(マッピング)を行ってトレーニングする時間が、単一のイテレーションの総和となります。
並列マッピング
では、同じ事前処理関数を使用して、複数のサンプルで並列に適用してみましょう。
データプロットが示すように、事前処理ステップがオーバーラップしたことで、単一のイテレーションにかかる総合時間が短縮されたことがわかります。
キャッシング
tf.data.Dataset.cache
変換は、メモリまたはローカルストレージのいずれかに、データセットをキャッシュすることができるため、各エポック中に一部の操作(ファイルを開いてデータを読み取るなど)が実行されなくなります。
ここでは、データ実行時間プロットは、データセットをキャッシュすると、cache
1 の前の変換(ファイルを開いてデータを読み取るなど)は、最初のエポックにのみ実行されることを示しています。次のエポックは、cache
変換によってキャッシュされたデータを再利用するようになります。
map
変換に渡されるユーザー定義関数が高くつく場合は、map
変換の後に cache
変換を適用することができますが、これは、キャッシュされるデータセットがメモリやローカルストレージにまだ格納できる場合に限ります。ユーザー定義関数によってデータセットを格納するために必要な容量がキャッシュのキャパシティを超えるほど増加する場合は、cache
変換の後に適用するようにするか、トレーニングジョブの前にデータを事前処理することでリソースの使用率を抑えることを検討してください。
マッピングのベクトル化
map
変換に渡されたユーザー定義関数を呼び出すと、ユーザー定義関数のスケジューリングと実行に関連するオーバーヘッドが生じます。ユーザー定義関数をベクトル化し(1 つの入力バッチでまとめて操作させる)、map
変換の前に batch
変換を適用してください。
これに適した実践を示すには、artificial データセットは適していません。スケジューリングの遅延は約 10 マイクロ秒(10e-6 秒)であり、ArtificialDataset
で使用される数十ミリ秒よりはるかに短いため、その影響がわかりづらいからです。
この例では、基本の tf.data.Dataset.range
関数を使用し、トレーニングループを最も単純な形態まで単純化します。
スカラマッピング
上の図は、何が起きているかを示しています(より少ないサンプル数で)。マッピングされた関数が各サンプルに適用されているのがわかります。この関数は非常に高速ですが、時間パフォーマンスに影響するオーバーヘッドがあります。
ベクトル化されたマッピング
今度は、マッピングされた関数は一度だけ呼び出され、サンプルのバッチに適用されています。データ実行時間のプロットが示すように、関数の実行にかかる時間は長くなりますが、オーバーヘッドの発生は一度だけであり、総合的な時間パフォーマンスが改善されています。
メモリフットプリントの縮小
interleave
、prefetch
、および shuffle
といった多数の変換は、要素の内部バッファにとどまります。map
変換に渡されるユーザー定義関数が要素のサイズを変更すると、map 変換の順序付けと、要素をバッファリングする変換によって、メモリ使用率に影響が及びます。通常、パフォーマンスの目的でほかの順序が求められない限り、メモリフットプリントがより少なくなる順序を選択してください。
部分計算のキャッシング
メモリに入りきれないほどのデータに増加する場合を除き、map
変換の後にデータセットをキャッシュすることが推奨されます。マッピングされた関数を、時間を消費するものとメモリを消費するものの 2 つに分割できれば、トレードオフを解消することができます。この場合、次のように変換をつなぐことができます。
こうすることで、時間を消費する部分は最初のエポック中にのみ実行されるようになるため、キャッシュスペースを使いすぎなくて済みます。
ベストプラクティスのまとめ
性能の高い TensorFlow 入力パイプライン設計のベストプラクティスをまとめてましょう。
prefetch
変換を使用して、プロデューサとコンシューマの作業をオーバーラップさせる。interleave
変換を使用して、データの読み取り変換を並列化する。num_parallel_calls
引数を設定して、map
変換を並列化する。cache
変換を使用して、最初のエポック中にデータをメモリにキャッシュする。map
変換に渡されるユーザー定義関数をベクトル化する。interleave
、prefetch
、およびshuffle
変換を適用する際に、メモリ使用率を低減する。
数値の再現
注意: これ以降のノートブックでは、上記の数値を再現する方法を説明しています。このコードを自由に調整してかまいませんが、このチュートリアルの要点ではないことに留意してください。
tf.data.Dataset
API の理解をさらに深めるには、独自のパイプラインで調整を試すのがよいでしょう。以下は、このガイドの画像を作成するために使用したコードです。次のような一般的な課題の回避策を示しているため、出発点にはご利用ください。
実行時間の再現可能性
マッピングされた関数の Eager execution
interleave
変換のコーラブル
データセット
ArtificialDataset
と同様に、各ステップにかかった時間を返すデータセットを構築できます。
このデータセットは、形状 [[2, 1], [2, 2], [2, 3]]
と型 [tf.dtypes.string, tf.dtypes.float32, tf.dtypes.int32]
のサンプルを提供します。各サンプルは、次のとおりです。
次のように解釈してください。
Open
とRead
はステップ識別子t0
は、対応するステップが開始した時間のタイムスタンプd
は、対応するステップにかかった時間i
はインスタンスのインデックスe
はエポックのインデックス(データセットがイテレートした回数)s
はサンプルのインデックス
イテレーションループ
すべてのタイミングを収集できるように、イテレーションループを多少複雑化するとよいでしょう。これは、上記に説明したサンプルを生成するデータセットでのみ機能します。
作図方法
最後に、timelined_benchmark
関数によって返された値でタイムラインを作図できる関数を定義します。
マッピングされた関数にラッパーを使用
マッピングされた関数を Eager コンテキストで実行するには、それらをtf.py_function
呼び出し内にラップする必要があります。