TensorFlow テストのベストプラクティス

TensorFlow リポジトリのコードをテストする際に推奨されるいくつかのプラクティスがあります。

はじめる前に

TensorFlow プロジェクトにソースコードを提供する前に、プロジェクトの GitHub リポジトリにある CONTRIBUTING.md ファイルを確認してください。(たとえば、コアの TensorFlow リポジトリの CONTRIBUTING.md ファイルを参照してください)。すべてのコントリビュータは、コントリビュータライセンス契約(CLA)に署名する必要があります。

一般的な原則

BUILD ルールで使用するもののみに依存すること

TensorFlow は大規模なライブラリであり、サブモジュールの単体テストを書く際には完全なパッケージに依存するのが一般的です。しかし、これは bazel 依存関係ベースの分析を無効化します。したがって、継続的インテグレーションシステムは送信前/送信後の実行とは無関係のテストをインテリジェントに排除できません。BUILD ファイルでテストしているサブモジュールのみに依存すれば、TensorFlow 開発者全員の時間と貴重な計算能力を大幅に節約できます。

ただし、ビルドの依存関係を変更して完全な TF のターゲットを省略すると、Python コードにインポートできるものにいくつかの制限が生じます。単体テストでは import tensorflow as tf ステートメントを使用することはできなくなります。しかし、開発者全員が多数の不要なテストを実行する必要がなくなるため、妥協する価値はあります。

すべてのコードに単体テストを用意すること

どんなコードを書く場合でも、対応する単体テストも書くべきです。新たに foo.py というファイルを書く場合は foo_test.py に対応する単体テストを書き、同じ変更のタイミングで送信する必要があります。すべてのコードで 90% 以上のインクリメンタルテストをカバーすることを目指してください。

TF でネイティブな bazel のテストルールを使用しないこと

TF のテスト実行には微妙な違いがたくさんあります。私たちはこのような複雑さすべてを bazel のマクロに隠すように努力してきました。このような複雑さに対処する必要性をなくすには、ネイティブなテストルールの代わりに次のルールを使用するようにしてください。これらはすべて tensorflow/tensorflow.bzl で定義されています。CC のテストには tf_cc_testtf_gpu_cc_testtf_gpu_only_cc_test を使用してください。Python のテストには tf_py_testgpu_py_test を使用してください。ネイティブの py_test ルールに非常に近いものが必要であれば、tensorflow.bzl で定義されているものを代わりに使用してください。BUILD ファイルの先頭に load(“tensorflow/tensorflow.bzl”, “py_test”) の一行を追加するだけです。

テストの実行場所に注意すること

テストを書く際には、私たちのテストインフラを使ってテストを CPU、GPU、アクセラレータで実行できます(対応するコードを書いている場合)。また、Linux / macos / windows(GPU あり / なし)でテストを自動実行できるようにしています。上記のマクロのいずれかを選択肢、タグを使用して実行場所を制限するだけです。

  • manual タグを使用すると、テストはどこからも実行されなくなります。bazel test tensorflow/… のようなパターンを使用する手動テストの実行も対象に含まれます。

  • no_oss は、テストを公式の TF OSS テストインフラストラクチャでの実行対象から除外します。

  • no_mac または no_windows タグを使用すると、テストを対応するオペレーティングシステムのテストスイートから除外できます。

  • no_gpu タグを使用すると、テストを GPU テストスイートでの実行対象から除外できます。

期待するテストスイートでテストが実行されることを確認すること

TF にはかなりの数のテストスイートがあります。時にはセットアップに戸惑うこともあるかもしれません。さまざまな問題が発生し、テストが継続的ビルドの対象から除外される可能性もあります。したがって、テストが期待どおりに実行されていることを確認する必要があります。そのためには、以下を実施してください。

  • プルリクエスト(PR)の事前送信が完了するのを待ちます。
  • PR の一番下までスクロールし、ステータスチェックを確認します。
  • Kokoro チェックの右側にある “Details” リンクをクリックします。
  • “Targets” リストをチェックし、新たに追加されたターゲットを見つけます。

各クラス/ユニットに独自の単体テストを用意すること

個別のテストクラスを使用すると、障害とリソースをより適切に分離できます。これにより、テストファイルがはるかに短くなり、可読性が向上します。したがって、すべての Python ファイルには一つ以上のテストファイルが必要です(それぞれの foo.py に対し、foo_test.py を用意する必要があります)。さまざまなセットアップを要する統合テストなど、より複雑なテストの場合はテストファイルを追加するとよいでしょう。

速度と実行時間

可能な限りシャーディングを使用しないこと

シャーディングの代わりに以下を考慮してください。

  • テストを小さくする
  • 上記が不可能な場合はテストを分割する

シャーディングはテスト全体の遅延を減らす効果がありますが、同じ事はテストをより小さなターゲットに分割することで達成できます。テストを分割することで、各テストをより細かく制御できるようになり、不要な事前送信の実行が最小限に抑えられ、 不正な振る舞いをするテストケースを原因とする buildcop によるターゲット全体の無効化によるカバレッジの低下が減少します。さらに、シャーディングではすべてのシャードに対してすべてのテスト初期化コードが実行されるなど、あまり明確ではない隠れたコストが発生します。この問題は、余計な負荷の発生源としてインフラチームから報告されています。

テストは小規模であるほど優れているということ

テストを高速に実行できるほど、テストが実行される可能性も高くなります。テストの時間が 1 秒長くなると、開発者やインフラストラクチャによるテストの実行時間が累積で数時間程度長くなる可能性があります。テストは 30 秒未満で実行されるようにし(非 opt モードで)、小規模にしてください。テストを中規模としてマークするのは最後の手段にしてください。インフラは送信前や送信後には大きなテストを実行しません!そのため、大規模なテストは実行場所を調整する場合のみ書くようにしてください。以下は、テストをより高速に実行するためのヒントをです。

  • テストでのトレーニングイテレーションの実行回数を減らす。
  • 依存性注入を使用してテスト中のシステム内にある大量の依存関係を単純なフェイクに置換することを検討してください。
  • 単体テストでより小さな入力データを使用することを検討する。
  • 他の対策が効果的でない場合は、テストファイルを分割してみてください。

不安定性を解消するためにテスト規模に対応するタイムアウトの半分をテスト時間の目標とすること

bazel テストターゲットでは、小規模テストのタイムアウトは 1 分です。中規模テストのタイムアウトは 5 分です。大規模テストは単に TensorFlow のテストインフラでは実行されません。ただし、多くのテストでは所要時間は確定的ではありません。テストはさまざまな理由で追加の時間を必要とする場合があります。また、平均で 50 秒間実行されるテストを小規模とマークした場合、旧型の CPU を搭載したマシンでスケジュールされたテストは不安定になります。そのため、小規模なテストでは平均実行時間を 30 秒にすることを目標にしてください。中規模のテストでは平均実行時間を 2 分 30 秒にすることを目標にしてください。

サンプル数を減らしてトレーニングの許容範囲を広げること

実行速度の遅いテストは共同作業者の妨げとなります。テストでのトレーニングの実行は非常に遅くなる可能性があります。テストで十分な速度(最大 2.5 分)を維持するには、サンプル数を減らせるように許容範囲を広げることが望ましいです。

不確定性と不安定性の解消

確定的なテストを書くこと

単体テストは常に確定的ではなければなりません。TAP と guitar で実行されるすべてのテストは、テストに影響するコードの変更がない限り常に同じように実行されるべきです。そのためには、以下のいくつかの点を考慮する必要があります。

常に偶然性の源にシードを与えること

ランダムジェネレータやその他の偶然性の源となるものはすべて不安定性を引き起こす可能性があります。そのため、これらのそれぞれに対してシードを付える必要があります。そうすることで、テストの不安定性を抑えることができ、すべてのテストが再現可能になります。以下は、TF のテストで設定する必要がありそうなシードのさまざまな設定方法です。

# Python RNG
import random
random.seed(42)

# Numpy RNG
import numpy as np
np.random.seed(42)

# TF RNG
from tensorflow.python.framework import random_seed
random_seed.set_seed(42)

マルチスレッドのテストでは sleep の使用を避けること

テストでの sleep 関数の使用は不安定性の主な原因となる可能性があります。特にマルチスレッドを使用する場合、sleep を使用して別のスレッドを待機すると確定的な結果は得られません。システムが異なるスレッドやプロセスの実行順序を保証できないためです。そのため、ミューテックスのような確定的な同期機構が望ましいです。

テストが不安定かどうかを確認すること

不安定性は buildcop と開発者の多くの時間を無駄にします。不安定性は検出もデバッグも難しいものです。不安定性を検出する自動化システムも存在しますが、そのようなシステムは拒否リストを正確に構築するまで多数のテストを何度も実行しなければなりません。仮にテストが検出されたとしても、そのテストは拒否リストに追加されてテストカバレッジは失われてしまいます。そのため、テスト作成者はテストを書く際にテストが不安定かどうかを確認する必要があります。--runs_per_test=1000 というフラグを付けてテストを実行すると簡単に確認できます。

TensorFlowTestCase を使用すること

TensorFlowTestCase は不安定性を可能な限り排除するため、使用されるすべての乱数ジェネレータにシードを与えるなどの必要な予防策を講じます。より多くの不安定なソースを検出して修正すると、それらのソースは TensorFlowTestCase に追加されます。そのため、tensorflow のテストを書く際には TensorFlowTestCase を使用する必要があります。TensorFlowTestCase は tensorflow/python/framework/test_util.py で定義されています。

閉鎖環境テストを書くこと

閉鎖環境テストは外部のリソースを必要としません。このようなテストには必要なものがすべて含まれており、必要になる可能性のあるフェイクサービスを起動するだけで済みます。テスト以外のすべてのサービスは確定性を損ねる原因となります。他のサービスの可能性が 99% だったとしても、ネットワークが不安定になり、rpc 応答が遅延し、最終的にに不可解なエラーメッセージが表示される可能性があります。外部サービスには GCS、S3、または任意の Web サイトがあります(これらに限定するわけではありません)。