TensorFlow 2.0 Alpha : ガイド : tf.data パフォーマンス (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 03/23/2019
* 本ページは、TensorFlow の本家サイトの TF 2.0 Alpha の以下のページを翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
ガイド : tf.data パフォーマンス
概要
GPU と TPU は単一の訓練ステップを実行するために必要な時間を劇的に減少させます。最高点のパフォーマンスの獲得は、現在のステップが終了する前に次のステップのためのデータを引き渡す効率的な入力パイプラインが必要です。tf.data API は柔軟で効率的な入力パイプラインを構築する手助けをします。このドキュメントは様々なモデルとアクセラレータに渡る非常にパフォーマンスが高い TensorFlow 入力パイプラインを構築するための tf.data API の特徴とベストプラクティスを説明します。
このガイドは次を行ないます :
- TensorFlow 入力パイプラインが本質的には ETL プロセスであることを示す。
- 高パフォーマンスな TensorFlow 入力パイプラインを設計するための推奨プラクティスを記述する
- 変換を適用する順序のパフォーマンスの影響を議論する。
入力パイプライン構造
典型的な TensorFlow 訓練入力パイプラインは ETL プロセスとして構成できます :
- Extract (抽出) : メモリ (NumPy) または永続性ストレージ — ローカル (HDD or SSD) またはリモート (e.g. GCS or HDFS) — からデータを読みます。
- Transform (変換, 変形): CPU を使用して parse してシャッフル、バッチ処理のようなデータ上の前処理演算、そして画像デコンプレッションのようなドメイン固有の変換そして増強、テキストベクトル化、あるい動画の時間的サンプリングを遂行します。
- Load (ロード): 変換されたデータを機械学習モデルを実行するアクセラレータ・デバイス (e.g. GPU(s) or TPU(s)) 上にロードします。
このパターンはアクセラレータを貴方のモデルを訓練する重労働のために取っておく一方で、CPU を効果的に活用します。更に、入力パイプラインを ETL プロセスとして見ることはパフォーマンス最適化の適用を容易にするフレームワークを提供します。
下のサンプルは、ラベル付けされた画像を含む TFRecord ファイルを読みそしてそれらを訓練に適した画像-ラベル・ペアのバッチに変換する入力パイプラインの素朴な実装を表します。入力パイプラインは tf.data.Dataset として表わされます、これは tf.keras のような高位 TensorFlow APIに渡すことができます。
def parse_fn(example): "Parse TFExample records and perform simple data augmentation." example_fmt = { "image": tf.FixedLengthFeature((), tf.string, ""), "label": tf.FixedLengthFeature((), tf.int64, -1) } parsed = tf.parse_single_example(example, example_fmt) image = tf.io.image.decode_image(parsed["image"]) image = _augment_helper(image) # augments image using slice, reshape, resize_bilinear return image, parsed["label"] def make_dataset(): dataset = tf.data.TFRecordDataset("/path/to/dataset/train-*.tfrecord") dataset = dataset.shuffle(buffer_size=FLAGS.shuffle_buffer_size) dataset = dataset.map(map_func=parse_fn) dataset = dataset.batch(batch_size=FLAGS.batch_size) return dataset
次のセクションはこの入力パイプラインを利用して、高パフォーマンスな TensorFlow 入力パイプラインを設計するためのベストプラクティスを示します。
パフォーマンスを最適化する
(GPU と TPU のような) 新しい計算デバイスがニューラルネットワークをどんどん高速に訓練することを可能にしますので、CPU 処理がボトルネックになりがちです。tf.data API は CPU を効果的に利用して ETL プロセスの各ステップを最適化する入力パイプラインを設計するためのビルディングブロックをユーザに提供します。
パイプライン処理
訓練ステップを遂行するためには、最初に訓練データを抽出して変換してそれからそれをアクセラレータで動作するモデルに供給しなければなりません。けれども、素朴な同期実装では、CPU がデータを準備している間、アクセラレータはアイドリングしています。反対に、アクセラレータがモデルを訓練している間、CPU はアイドリングしています。こうして訓練ステップ時間は CPU 前処理時間とアクセラレータ訓練時間の両者の総計です。
パイプライン処理 は前処理と訓練ステップのモデル実行をオーバーラップさせます。アクセラレータが訓練ステップ N を遂行している間、CPU はステップ N+1 のためのデータを準備しています。そのようにすることはステップ時間を訓練とデータを抽出して変換するのにかかる時間の (総計と対照的に) 最大値にまで減じます。
パイプライン処理なしでは、CPU と GPU/TPU は殆どの時間アイドリングします :
パイプライン処理では、アイドル時間は著しく減少します :
tf.data API は tf.data.Dataset.prefetch 変換を通してソフトウェア・パイプライン処理機構を提供します、これはデータが生成される時をデータが消費される時から切り離します。特に、変換は要求される前に入力データセットから要素を先取りする (= prefetch) ためにバックグラウンド・スレッドと内部バッファを使用します。先取りする要素の数は単一の訓練ステップにより消費されるバッチの数と同じ (あるいは多分それ以上) であるべきです。この値を手動で調整するかそれを tf.data.experimental.AUTOTUNE に設定することもできるでしょう、これは tf.data ランタイムに実行時に動的に値を調整することを促します、
この変更を私達の実行サンプルに適用するためには、次を :
dataset = dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
貴方の入力パイプラインの最後の変換として挿入します。
prefetech 変換は “producer” のワークを “consumer” のワークとオーバーラップさせる機会があればいつでも利益をもたらすことに注意してください。
データ変換を並列化する
バッチを準備するとき、入力要素は前処理される必要があるかもしれません。この目的のために、tf.data API は tf.data.Dataset.map 変換を提供します、これは入力データセットの各要素にユーザ定義関数 (例えば、実行サンプルからの parse_fn) を適用します。入力要素は互いに無関係なので、前処理はマルチ CPU コアに渡り並列化できます。これを可能にするために、map 変換は並列処理のレベルを指定する num_parallel_calls 引数を提供します。例えば、次の図は map 変換に num_parallel_calls=2 を設定する効果を示します :
num_parallel_calls 引数のための最善の値を選択することはハードウェア、(サイズと shape のような) 訓練データの特性、map 関数のコスト、そして何の他の処理が同時に CPU 上で起きているかに依拠します ; 単純な経験則は利用可能な CPU コアの数を使用します。例えば、上のサンプルを実行するマシンが 4 コアを持つ場合、それは num_parallel_calls=4 を設定することはより効率的でしょう。その一方で、num_parallel_calls を利用可能な CPU の数よりも遥かに大きい値に設定すると非効率的なスケジューリングに繋がり、スローダウンの結果になる可能性があります。prefetch 変換と同様に、map 変換は tf.data.experimental.AUTOTUNE をサポートします、これはどのレベルの並列処理を使用するかについての決定を tf.data ランタイムに委ねます。
実行サンプルにこの変更を適用するには、次を :
dataset = dataset.map(map_func=parse_fn)
次で置き換えます :
dataset = dataset.map(map_func=parse_fn, num_parallel_calls=tf.data.experimental.AUTOTUNE)
データ抽出を並列化する
現実世界の設定では、入力データはリモートにストアされるかもしれません (例えば、GCS や HDFS)、何故ならば入力データがローカルに収まらないとか、訓練が分散されていて入力データを総てのマシン上に複製することが無意味であるとかの理由です。ローカルでデータを読む時には上手く動作したデータセット・パイプラインは、ローカルとリモート・ストレージ間の次の違いによりデータをリモートに読む時 I/O 上のボトルネックになるかもしれません。
- 最初のバイトへの時間: リモートストレージから最初のバイトを読むのはローカルストレージからよりも桁違いに長くかかる可能性があります。
- 読み込みスループット: リモートストレージが巨大な集合的な帯域を提供する一方で、単一のファイルを読むことはこの帯域の小さな断片を利用できるだけかもしれません。
加えて、ひとたび生バイトがメモリに読み込まれれば、データ (e.g. probobuf) をデシリアライズ and/or 解読する必要もあるかもしれません、これは追加の計算を必要とします。このオーバーヘッドはデータがローカルかリモートにストアされているかに無関係に存在します。
様々なデータ抽出オーバーヘッドのインパクトを migrate するために、データ抽出ステップを並列化するために tf.data.Dataset.interleave 変換が使用できて、(データファイル・リーダーのように) 他のデータセットの内容をさしはさみます。オーバーラップさせるデータセットの数は cycle_length 引数により指定され、一方で並列レベルは num_parallel_calls 引数により指定されます。prefetch と map 変換と同様に、interleave 変換は tf.data.experimental.AUTOTUNE をサポートします、これは並列性のどのレベルを使用するかについての決定を tf.data ランタイムに委ねます。
次の図は interleave 変換に cycle_length=2 と num_parallel_calls=2 を供給する効果を示します :
この変更を訓練サンプルに適用するためには、次を :
dataset = tf.data.TFRecordDataset("/path/to/dataset/train-*.tfrecord")
次で置き換えます :
files = tf.data.Dataset.list_files("/path/to/dataset/train-*.tfrecord") dataset = files.interleave( tf.data.TFRecordDataset, cycle_length=FLAGS.num_parallel_reads, num_parallel_calls=tf.data.experimental.AUTOTUNE)
パフォーマンス考察
tf.data API は構成可能な変換まわりをユーザに柔軟性を提供するために設計されています。これらの変換の多くは可搬ですが、特定の変換の順序はパフォーマンスとの関係を持ちます。
Map と Batch
map 変換に渡されたユーザ定義関数の起動はスケジューリングとユーザ定義関数の実行に関連するオーバーヘッドを持ちます。通常は、このオーバーヘッドは関数により遂行される計算の総量に比較して小さいです。けれども、map が殆ど作業をしない場合には、このオーバーヘッドは総コストを占める可能性があります。そのような場合、ユーザ定義関数のベクトル化を使用します (つまり、それを入力のバッチに渡り一度に演算させます)、そして map 変換の前に batch 変換を適用します。
Map と Cache
tf.data.Dataset.cache 変換はデータセットをメモリかローカルストレージにキャッシュできます。map 変換に渡されるユーザ定義関数が高価である場合、map 変換の後に cache 変換を適用します、結果データセットが依然としてメモリかローカルストレージに収まる限りは。ユーザ定義関数がキャッシュ容量を越えてデータセットをストアする必要がある空間を増加させる場合には、リソース使用を減じるために訓練ジョブの前にデータの前処理を考えてください。
Map と Interleave / Prefetch / Shuffle
interleave, prefetch と shuffle を含む多くの変換は要素の内部バッファを維持します。map 変換に渡されるユーザ定義関数が要素の数を変更する場合、map 変換と要素をバッファリングする変換の順序はメモリ使用に影響を与えます。一般に、(例えば、map と batch 変換の融合を有効にするために) パフォーマンスのために異なる順序が望ましいのでなければ、より低いメモリフットプリントの結果になる順序を選択することを推奨します。
Repeat と Shuffle
tf.data.Dataset.repeat 変換は入力データを有限 (or 無限) 回数繰り返します ; データの各反復は典型的にはエポックとして参照されます。tf.data.Dataset.shuffle 変換はデータセット・サンプルの順序をランダム化します。
repeat 変換が shuffle 変換の前に適用される場合、エポック境界は曖昧になります。つまり、他の要素が一度さえ現れる前に、ある要素が繰り返される可能性があります。他方、repeat 変換の前に shuffle 変換が適用される場合、shuffle 変換の初期状態の初期化に関連して各エポックの最初にスローダウンするかもしれません。換言すれば、前者 (repeat before shuffle) はより良いパフォーマンスを提供し、その一方で後者 (shuffle before repeat) はより強い順序の保証を提供します。
ベストプラクティスの要約
高パフォーマンスな TensorFlow 入力パイプラインを設計するためのベストプラクティスの要約がここにあります :
- producer と consumer のワークをオーバラップさせるために prefetch 変換を使用します。訓練はアクセラレータ上で成されながら、特に、CPU 上で遂行される変換群をオーバーラップさせるために入力パイプラインの最後に prefetch を追加することを勧めます。バッファサイズを手動で調整するか、決定を tf.data ランタイムに委ねるために tf.data.experimental.AUTOTUNE を使用します。
- num_parallel_calls 引数を設定することにより map 変換を並列化します。並列レベルを手動で調整するか、決定を tf.data ランタイムに委ねるために tf.data.experimental.AUTOTUNE を使用します。
- リモートにストアされている and/or デシリアライゼーションを必要とするデータで作業している場合、異なるファイルからのデータの読み込み (とデシリアライゼーション) を並列化するために interleave 変換を使用することを勧めます。
- map 変換に渡される安価なユーザ定義関数を、スケジューリングと関数実行に関連するオーバーヘッドを償却するためにベクトル化します。
- 貴方のデータがメモリに収まる場合、最初のエポックの間にそれをメモリにキャッシュするために cache 変換を使用してください、その結果続くエポックはそれを読み込み、パーシングして変換することに関連するオーバヘッドを回避できます。
- 前処理が貴方のデータのサイズを増やす場合には、メモリ使用を減らすために interleave, prefetch と (可能であれば) shuffle を最初に適用することを勧めます。
- repeat 変換の前に shuffle 変換を適用することを勧めます。
以上