本頁面由 Cloud Translation API 翻譯而成。
Switch to English

使用TF Profiler分析tf.data性能

總覽

本指南假定您熟悉TensorFlow Profilertf.data 。它旨在通過示例逐步提供說明,以幫助用戶診斷和修復輸入管道性能問題。

首先,收集您的TensorFlow作業的配置文件。有關如何執行此操作的說明,可用於CPU / GPUCloud TPU

TensorFlow Trace Viewer

下面詳細介紹的分析工作流程著重於Profiler中的跟踪查看器工具。該工具顯示時間軸,該時間軸顯示TensorFlow程序執行的操作的持續時間,並允許您確定執行時間最長的操作。有關跟踪查看器的更多信息,請查看TF Profiler指南的此部分 。通常, tf.data事件將出現在主機CPU時間軸上。

分析工作流程

請遵循以下工作流程。如果您有反饋來幫助我們改進它,請創建一個標籤為“ comp:data” 的github問題

1.您的tf.data管道生成數據的速度足夠快嗎?

首先確定輸入管道是否是TensorFlow程序的瓶頸。

這樣做,在跟踪查看器中查找IteratorGetNext::DoCompute ops。通常,您希望在步驟開始時就能看到這些內容。這些切片表示輸入管道在被請求時產生一批元素所花費的時間。如果您正在使用keras或在tf.function遍歷數據集,則應在tf_data_iterator_get_next線程中找到tf_data_iterator_get_next

請注意,如果使用分配策略 ,則可能會看到IteratorGetNextAsOptional::DoCompute事件,而不是IteratorGetNext::DoCompute (從TF 2.3開始)。

image

如果呼叫迅速返回(<= 50 us),則意味著您的數據在需要時可用。輸入管道不是您的瓶頸;有關更多常規性能分析提示,請參見Profiler指南

image

如果呼叫返回緩慢,則 tf.data無法滿足使用者的請求。繼續下一節。

2.您是否正在預取數據?

輸入管道性能的最佳做法是在tf.data管道的末尾插入一個tf.data.Dataset.prefetch轉換。此轉換將輸入管道的預處理計算與模型計算的下一步重疊,是訓練模型時實現最佳輸入管道性能所必需的。如果要預取數據,則應該在與IteratorGetNext::DoCompute op相同的線程上看到Iterator::Prefetch切片。

image

如果您在管道的末尾沒有prefetch ,則應添加一個。有關tf.data性能建議的更多信息,請參閱《 tf.data性能指南》

如果您已經在預取數據 ,而輸入管道仍然是您的瓶頸,請繼續下一節以進一步分析性能。

3.您是否達到較高的CPU利用率?

tf.data通過嘗試最大程度地利用可用資源來實現高吞吐量。通常,即使在GPU或TPU等加速器上運行模型時, tf.data管道也在CPU上運行。您可以使用sarhtop之類的工具來檢查您的利用率,或者如果您在GCP上運行,則可以在雲監視控制台中檢查它的利用率。

如果您的利用率低,則表明您的輸入管道可能沒有充分利用主機CPU。您應查閱tf.data性能指南以獲取最佳做法。如果您應用了最佳實踐,並且利用率和吞吐量仍然很低,請繼續下面的瓶頸分析

如果您的利用率接近資源極限 ,為了進一步提高性能,則需要提高輸入管道的效率(例如,避免不必要的計算)或卸載計算。

通過避免tf.data不必要的計算,可以提高輸入管道的效率。一種實現方法是,如果您的數據適合內存,則在需要大量計算的工作之後插入tf.data.Dataset.cache轉換。這以增加內存使用為代價減少了計算。此外,禁用tf.data中的運算tf.data內部並行tf.data有可能使效率提高> 10%,並且可以通過在輸入管道上設置以下選項來完成:

dataset = ...
options = tf.data.Options()
options.experimental_threading.max_intra_op_parallelism = 1
dataset = dataset.with_options(options)

4.瓶頸分析

下一節將tf.data介紹如何在跟踪查看器中讀取tf.data事件,以了解瓶頸在哪里以及可能的緩解策略。

了解事件探查器中的tf.data事件

事件探查器中的每個tf.data事件都具有名稱Iterator::<Dataset> ,其中<Dataset>是數據集源或轉換的名稱。每個事件還具有長名稱Iterator::<Dataset_1>::...::<Dataset_n> ,您可以通過單擊tf.data事件來看到它。在長名稱中, <Dataset_n>匹配(短)名稱中的<Dataset> ,長名稱中的其他數據集表示下游轉換。

image

例如,上面的屏幕截圖是從以下代碼生成的:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)

在這裡, Iterator::Map事件的長名稱為Iterator::BatchV2::FiniteRepeat::Map 。請注意,數據集名稱可能與python API略有不同(例如,使用FiniteRepeat而不是Repeat),但應足夠直觀以進行解析。

同步和異步轉換

對於同步tf.data轉換(例如BatchMap ),您將在同一線程上看到來自上游轉換的事件。在上面的示例中,由於使用的所有轉換都是同步的,因此所有事件都出現在同一線程上。

對於異步轉換(例如PrefetchParallelMapParallelInterleaveMapAndBatch ),來自上游轉換的事件將在不同的線程上。在這種情況下,“長名稱”可以幫助您確定事件對應於管道中的哪個轉換。

image

例如,上面的屏幕截圖是從以下代碼生成的:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)
dataset = dataset.prefetch(1)

在這裡, Iterator::Prefetch事件在tf_data_iterator_get_next線程上。由於Prefetch是異步的,因此其輸入事件( BatchV2 )將位於不同的線程上,可以通過搜索長名稱Iterator::Prefetch::BatchV2 。在這種情況下,它們位於tf_data_iterator_resource線程上。從其長名稱中,您可以推斷出BatchV2Prefetch上游。此外, BatchV2事件的parent_id將與Prefetch事件的ID匹配。

識別瓶頸

通常,要確定輸入管道中的瓶頸,請從最外面的轉換一直到源代碼遍歷輸入管道。從管道中的最終轉換開始,遞歸到上游轉換,直到找到緩慢的轉換或到達源數據集,例如TFRecord 。在上面的示例中,您將從Prefetch開始,然後向上游BatchV2FiniteRepeatMap ,最後是Range

通常,慢速轉換對應於事件長而輸入事件短的轉換。下面是一些示例。

請注意,大多數主機輸入管道中的最終(最外部)轉換是Iterator::Model事件。模型轉換是由tf.data運行時自動引入的,用於檢測和自動調整輸入管道的性能。

如果您的工作使用分配策略 ,則跟踪查看器將包含與設備輸入管道相對應的其他事件。設備管道的最外層轉換(位於IteratorGetNextOp::DoComputeIteratorGetNextAsOptionalOp::DoCompute )將是帶有上游Iterator::Generator事件的Iterator::Prefetch事件。您可以通過搜索Iterator::Model事件找到相應的主機管道。

例子1

image

上面的屏幕截圖是從以下輸入管道生成的:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record)
dataset = dataset.batch(32)
dataset = dataset.repeat()

在屏幕截圖中,觀察到(1) Iterator::Map事件很長,但是(2)其輸入事件( Iterator::FlatMap )快速返回。這表明順序Map轉換是瓶頸。

請注意,在屏幕截圖中, InstantiatedCapturedFunction::Run事件對應於執行map函數所花費的時間。

例子2

image

上面的屏幕截圖是從以下輸入管道生成的:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record, num_parallel_calls=2)
dataset = dataset.batch(32)
dataset = dataset.repeat()

此示例與上面的示例相似,但是使用ParallelMap而不是Map。我們在這裡註意到,(1) Iterator::ParallelMap事件很長,但是(2)它的輸入事件Iterator::FlatMap (由於ParallelMap是異步的,它們在不同的線程上)很短。這表明ParallelMap轉換是瓶頸。

解決瓶頸

源數據集

如果已將數據集源識別為瓶頸,例如從TFRecord文件中讀取數據,則可以通過並行化數據提取來提高性能。要做到這一點,確保你的數據在多個文件分片,並使用tf.data.Dataset.interleavenum_parallel_calls參數集tf.data.experimental.AUTOTUNE 。如果確定性對於您的程序不重要,則可以通過從TF 2.2開始在tf.data.Dataset.interleave上設置deterministic=False標誌來進一步提高性能。例如,如果您正在讀取TFRecords,則可以執行以下操作:

dataset = tf.data.Dataset.from_tensor_slices(filenames)
dataset = dataset.interleave(tf.data.TFRecordDataset,
  num_parallel_calls=tf.data.experimental.AUTOTUNE,
  deterministic=False)

請注意,分片文件應足夠大,以分攤打開文件的開銷。有關並行數據提取的更多詳細信息,請參閱tf.data性能指南的此部分

轉換數據集

如果您已將tf.data中間轉換識別為瓶頸,則可以通過並行化轉換或緩存計算(如果您的數據適合內存)來解決該問題。某些轉換(例如Map具有平行的對應項; 《 tf.data性能指南》演示瞭如何並行處理這些問題。其他轉換(例如FilterUnbatchBatch本質上是順序的。您可以通過引入“外部並行性”來並行化它們。例如,假設您的輸入管道最初看起來如下所示,並且將Batch作為瓶頸:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)
dataset = filenames_to_dataset(filenames)
dataset = dataset.batch(batch_size)

您可以通過在分片輸入上運行輸入管道的多個副本並組合結果來引入“外部並行性”:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)

def make_dataset(shard_index):
  filenames = filenames.shard(NUM_SHARDS, shard_index)
  dataset = filenames_to_dataset(filenames)
  Return dataset.batch(batch_size)

indices = tf.data.Dataset.range(NUM_SHARDS)
dataset = indices.interleave(make_dataset,
                             num_parallel_calls=tf.data.experimental.AUTOTUNE)
dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)

額外資源