Analisis kinerja tf.data dengan TF Profiler

Ringkasan

Panduan ini mengasumsikan pemahaman tentang TensorFlow Profiler dan tf.data . Ini bertujuan untuk memberikan petunjuk langkah demi langkah dengan contoh untuk membantu pengguna mendiagnosis dan memperbaiki masalah kinerja pipa masukan.

Untuk memulai, kumpulkan profil pekerjaan TensorFlow Anda. Petunjuk tentang cara melakukannya tersedia untuk CPU/GPU dan Cloud TPU .

TensorFlow Trace Viewer

Alur kerja analisis yang diperinci di bawah berfokus pada alat penampil jejak di Profiler. Alat ini menampilkan garis waktu yang menunjukkan durasi operasi yang dijalankan oleh program TensorFlow Anda dan memungkinkan Anda mengidentifikasi operasi mana yang membutuhkan waktu paling lama untuk dieksekusi. Untuk informasi selengkapnya tentang penampil jejak, lihat bagian panduan TF Profiler ini . Secara umum, event tf.data akan muncul di timeline CPU host.

Alur Kerja Analisis

Silakan ikuti alur kerja di bawah ini. Jika Anda memiliki umpan balik untuk membantu kami memperbaikinya, harap buat masalah github dengan label "comp:data".

1. Apakah pipeline tf.data Anda menghasilkan data dengan cukup cepat?

Mulailah dengan memastikan apakah pipeline input merupakan hambatan untuk program TensorFlow Anda.

Untuk melakukannya, cari operasi IteratorGetNext::DoCompute di trace viewer. Secara umum, Anda berharap untuk melihatnya di awal langkah. Irisan ini mewakili waktu yang dibutuhkan pipa input Anda untuk menghasilkan kumpulan elemen saat diminta. Jika Anda menggunakan keras atau mengulangi dataset Anda di tf.function , ini harus ditemukan di utas tf_data_iterator_get_next .

Perhatikan bahwa jika Anda menggunakan strategi distribusi , Anda mungkin melihat peristiwa IteratorGetNextAsOptional::DoCompute alih-alih IteratorGetNext::DoCompute (pada TF 2.3).

image

Jika panggilan kembali dengan cepat (<= 50 us), ini berarti data Anda tersedia saat diminta. Pipa input bukanlah hambatan Anda; lihat panduan Profiler untuk tips analisis kinerja yang lebih umum.

image

Jika panggilan kembali dengan lambat, tf.data tidak dapat memenuhi permintaan konsumen. Lanjutkan ke bagian berikutnya.

2. Apakah Anda melakukan prefetching data?

Praktik terbaik untuk performa pipeline input adalah menyisipkan transformasi tf.data.Dataset.prefetch di akhir pipeline tf.data Anda. Transformasi ini tumpang tindih dengan komputasi prapemrosesan pipeline input dengan langkah komputasi model berikutnya dan diperlukan untuk performa pipeline input yang optimal saat melatih model Anda. Jika Anda melakukan prefetching data, Anda akan melihat Iterator::Prefetch slice pada thread yang sama dengan IteratorGetNext::DoCompute op.

image

Jika Anda tidak memiliki prefetch di akhir pipeline , Anda harus menambahkannya. Untuk informasi selengkapnya tentang rekomendasi performa tf.data , lihat panduan performa tf.data .

Jika Anda sudah melakukan prapengambilan data , dan saluran masukan masih menjadi penghambat, lanjutkan ke bagian berikutnya untuk menganalisis kinerja lebih lanjut.

3. Apakah Anda mencapai utilisasi CPU yang tinggi?

tf.data mencapai throughput tinggi dengan mencoba memanfaatkan sumber daya yang tersedia sebaik mungkin. Secara umum, bahkan saat menjalankan model Anda di akselerator seperti GPU atau TPU, pipeline tf.data dijalankan di CPU. Anda dapat memeriksa penggunaan Anda dengan fitur seperti sar dan htop , atau di konsol pemantauan cloud jika Anda menggunakan GCP.

Jika pemanfaatan Anda rendah, ini menunjukkan bahwa pipa input Anda mungkin tidak memanfaatkan CPU host sepenuhnya. Anda harus berkonsultasi dengan panduan kinerja tf.data untuk praktik terbaik. Jika Anda telah menerapkan praktik terbaik dan utilisasi serta throughput tetap rendah, lanjutkan ke analisis Bottleneck di bawah ini.

Jika pemanfaatan Anda mendekati batas sumber daya , untuk meningkatkan kinerja lebih lanjut, Anda perlu meningkatkan efisiensi pipa masukan (misalnya, menghindari perhitungan yang tidak perlu) atau perhitungan offload.

Anda dapat meningkatkan efisiensi saluran masukan dengan menghindari perhitungan yang tidak perlu di tf.data . Salah satu cara untuk melakukan ini adalah menyisipkan transformasi tf.data.Dataset.cache setelah pekerjaan intensif komputasi jika data Anda masuk ke dalam memori; ini mengurangi perhitungan dengan biaya penggunaan memori yang meningkat. Selain itu, menonaktifkan paralelisme intra-op di tf.data berpotensi meningkatkan efisiensi > 10%, dan dapat dilakukan dengan menyetel opsi berikut pada pipeline input Anda:

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

4. Analisis Kemacetan

Bagian berikut menjelaskan cara membaca peristiwa tf.data di penampil jejak untuk memahami di mana hambatannya dan kemungkinan strategi mitigasi.

Memahami peristiwa tf.data di Profiler

Setiap kejadian tf.data di Profiler memiliki nama Iterator::<Dataset> , di mana <Dataset> adalah nama sumber atau transformasi kumpulan data. Setiap event juga memiliki nama panjang Iterator::<Dataset_1>::...::<Dataset_n> , yang dapat Anda lihat dengan mengklik event tf.data . Dalam nama panjang, <Dataset_n> cocok dengan <Dataset> dari nama (pendek), dan kumpulan data lain dalam nama panjang mewakili transformasi hilir.

image

Misalnya, tangkapan layar di atas dihasilkan dari kode berikut:

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

Di sini, acara Iterator::Map memiliki nama panjang Iterator::BatchV2::FiniteRepeat::Map . Perhatikan bahwa nama kumpulan data mungkin sedikit berbeda dari API python (misalnya, FiniteRepeat alih-alih Ulangi), tetapi harus cukup intuitif untuk diuraikan.

Transformasi sinkron dan asinkron

Untuk transformasi tf.data sinkron (seperti Batch dan Map ), Anda akan melihat peristiwa dari transformasi hulu pada utas yang sama. Dalam contoh di atas, karena semua transformasi yang digunakan bersifat sinkron, semua kejadian muncul di utas yang sama.

Untuk transformasi asinkron (seperti Prefetch , ParallelMap , ParallelInterleave dan MapAndBatch ) peristiwa dari transformasi hulu akan berada di utas yang berbeda. Dalam kasus seperti itu, "nama panjang" dapat membantu Anda mengidentifikasi transformasi mana dalam alur yang terkait dengan peristiwa.

image

Misalnya, tangkapan layar di atas dihasilkan dari kode berikut:

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

Di sini, acara Iterator::Prefetch ada di utas tf_data_iterator_get_next . Karena Prefetch asinkron, peristiwa masukannya ( BatchV2 ) akan berada di utas yang berbeda, dan dapat ditemukan dengan mencari nama panjang Iterator::Prefetch::BatchV2 . Dalam hal ini, mereka ada di utas tf_data_iterator_resource . Dari namanya yang panjang, Anda dapat menyimpulkan bahwa BatchV2 adalah upstream dari Prefetch . Selanjutnya, parent_id dari event BatchV2 akan cocok dengan ID dari event Prefetch .

Mengidentifikasi kemacetan

Secara umum, untuk mengidentifikasi bottleneck dalam pipa input Anda, atur pipa input dari transformasi terluar sampai ke sumbernya. Mulai dari transformasi terakhir di pipeline Anda, berulang ke transformasi upstream hingga Anda menemukan transformasi yang lambat atau mencapai kumpulan data sumber, seperti TFRecord . Pada contoh di atas, Anda akan mulai dari Prefetch , lalu berjalan ke upstream ke BatchV2 , FiniteRepeat , Map , dan terakhir Range .

Secara umum, transformasi lambat berhubungan dengan transformasi yang peristiwanya panjang, tetapi peristiwa masukannya singkat. Beberapa contoh ikuti di bawah ini.

Perhatikan bahwa transformasi terakhir (terluar) di sebagian besar saluran input host adalah peristiwa Iterator::Model . Transformasi Model diperkenalkan secara otomatis oleh runtime tf.data dan digunakan untuk menginstrumentasi dan menyetel otomatis performa pipeline input.

Jika tugas Anda menggunakan strategi distribusi , penampil jejak akan berisi peristiwa tambahan yang sesuai dengan pipa masukan perangkat. Transformasi terluar dari pipeline perangkat (bersarang di bawah IteratorGetNextOp::DoCompute atau IteratorGetNextAsOptionalOp::DoCompute ) akan menjadi peristiwa Iterator::Prefetch dengan peristiwa Upstream Iterator::Generator . Anda dapat menemukan pipa host yang sesuai dengan mencari peristiwa Iterator::Model .

Contoh 1

image

Tangkapan layar di atas dihasilkan dari pipa input berikut:

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

Di tangkapan layar, amati bahwa (1) Iterator::Map peristiwa panjang, tetapi (2) peristiwa masukannya ( Iterator::FlatMap ) kembali dengan cepat. Ini menunjukkan bahwa transformasi Peta berurutan adalah hambatannya.

Perhatikan bahwa di tangkapan layar, acara InstantiatedCapturedFunction::Run sesuai dengan waktu yang diperlukan untuk menjalankan fungsi peta.

Contoh 2

image

Tangkapan layar di atas dihasilkan dari pipa input berikut:

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

Contoh ini mirip dengan yang di atas, tetapi menggunakan ParallelMap, bukan Peta. Kami perhatikan di sini bahwa (1) acara Iterator::ParallelMap panjang, tetapi (2) acara masukannya Iterator::FlatMap (yang ada di utas berbeda, karena ParallelMap asinkron) pendek. Ini menunjukkan bahwa transformasi ParallelMap adalah hambatannya.

Mengatasi kemacetan

Kumpulan data sumber

Jika Anda telah mengidentifikasi sumber kumpulan data sebagai hambatan, seperti membaca dari file TFRecord, Anda dapat meningkatkan kinerja dengan memparalelkan ekstraksi data. Untuk melakukannya, pastikan data Anda di-sharding di beberapa file dan gunakan tf.data.Dataset.interleave dengan parameter num_parallel_calls disetel ke tf.data.AUTOTUNE . Jika determinisme tidak penting untuk program Anda, Anda dapat lebih meningkatkan kinerja dengan menyetel flag deterministic=False pada tf.data.Dataset.interleave pada TF 2.2. Misalnya, jika Anda membaca dari TFRecords, Anda dapat melakukan hal berikut:

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

Perhatikan bahwa file sharded harus berukuran cukup besar untuk mengamortisasi biaya pembukaan file. Untuk detail lebih lanjut tentang ekstraksi data paralel, lihat bagian panduan performa tf.data ini.

Kumpulan data transformasi

Jika Anda telah mengidentifikasi transformasi tf.data perantara sebagai hambatan, Anda dapat mengatasinya dengan memparalelkan transformasi atau menyimpan komputasi ke dalam cache jika data Anda cocok dengan memori dan sesuai. Beberapa transformasi seperti Map memiliki pasangan paralel; panduan kinerja tf.data menunjukkan cara memparalelkannya. Transformasi lain, seperti Filter , Unbatch , dan Batch secara inheren berurutan; Anda dapat memparalelkannya dengan memperkenalkan "paralelisme luar". Misalnya, misalkan pipa input Anda awalnya terlihat seperti berikut, dengan Batch sebagai hambatannya:

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

Anda dapat memperkenalkan "paralelisme luar" dengan menjalankan banyak salinan pipa input melalui input yang dipecah dan menggabungkan hasilnya:

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.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)

Sumber daya tambahan