Analisis kinerja tf.data dengan TF Profiler

Ringkasan

Panduan ini mengasumsikan keakraban dengan TensorFlow Profiler dan tf.data . Ini bertujuan untuk memberikan petunjuk langkah demi langkah dengan contoh untuk membantu pengguna mendiagnosis dan memperbaiki masalah kinerja saluran input.

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

TensorFlow Trace Viewer

Alur kerja analisis yang dirinci di bawah ini berfokus pada alat penampil pelacakan di Profiler. Alat ini menampilkan garis waktu yang menunjukkan durasi operasi yang dijalankan oleh program TensorFlow Anda dan memungkinkan Anda mengidentifikasi operasi mana yang paling lama dieksekusi. Untuk informasi lebih lanjut 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 meningkatkannya, buat masalah github dengan label "comp:data".

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

Mulailah dengan memastikan apakah pipeline input menjadi penghambat program TensorFlow Anda.

Untuk melakukannya, cari IteratorGetNext::DoCompute ops di trace viewer. Secara umum, Anda berharap untuk melihat ini di awal langkah. Irisan ini menunjukkan waktu yang diperlukan saluran input Anda untuk menghasilkan sekumpulan elemen saat diminta. Jika Anda menggunakan keras atau mengulangi set data Anda dalam 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 (mulai TF 2.3).

image

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

image

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

2. Apakah Anda melakukan prefetching data?

Praktik terbaik untuk kinerja pipeline input adalah dengan menyisipkan transformasi tf.data.Dataset.prefetch di akhir pipeline tf.data Anda. Transformasi ini tumpang tindih dengan perhitungan prapemrosesan pipeline input dengan langkah komputasi model berikutnya dan diperlukan untuk kinerja pipeline input yang optimal saat melatih model Anda. Jika Anda mengambil data sebelumnya, Anda akan melihat potongan Iterator::Prefetch pada utas yang sama dengan operasi IteratorGetNext::DoCompute .

image

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

Jika Anda sudah melakukan prefetching data , dan pipeline input masih menjadi hambatan Anda, lanjutkan ke bagian berikutnya untuk menganalisis kinerja lebih lanjut.

3. Apakah Anda mencapai utilisasi CPU yang tinggi?

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

Jika pemanfaatan Anda rendah, ini menunjukkan bahwa saluran 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 penggunaan Anda mendekati batas sumber daya , untuk meningkatkan kinerja lebih lanjut, Anda perlu meningkatkan efisiensi saluran input Anda (misalnya, menghindari komputasi yang tidak perlu) atau komputasi offload.

Anda dapat meningkatkan efisiensi saluran input Anda dengan menghindari perhitungan yang tidak perlu di tf.data . Salah satu cara untuk melakukan ini adalah memasukkan transformasi tf.data.Dataset.cache setelah pekerjaan intensif komputasi jika data Anda sesuai dengan memori; ini mengurangi komputasi dengan biaya peningkatan penggunaan memori. Selain itu, menonaktifkan paralelisme intra-op di tf.data berpotensi meningkatkan efisiensi hingga > 10%, dan dapat dilakukan dengan menyetel opsi berikut pada saluran 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 pelacakan untuk memahami di mana hambatan dan kemungkinan strategi mitigasi.

Memahami peristiwa tf.data di Profiler

Setiap peristiwa tf.data di Profiler memiliki nama Iterator::<Dataset> , dengan <Dataset> adalah nama sumber atau transformasi kumpulan data. Setiap peristiwa juga memiliki nama panjang Iterator::<Dataset_1>::...::<Dataset_n> , yang dapat Anda lihat dengan mengeklik peristiwa 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 python API (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 upstream pada thread yang sama. Dalam contoh di atas, karena semua transformasi yang digunakan adalah sinkron, semua peristiwa muncul di utas yang sama.

Untuk transformasi asinkron (seperti Prefetch , ParallelMap , ParallelInterleave dan MapAndBatch ) peristiwa dari transformasi upstream akan berada di utas yang berbeda. Dalam kasus seperti itu, "nama panjang" dapat membantu Anda mengidentifikasi transformasi mana dalam saluran yang sesuai 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 tidak sinkron, kejadian inputnya ( BatchV2 ) akan berada di utas yang berbeda, dan dapat ditemukan dengan mencari nama panjang Iterator::Prefetch::BatchV2 . Dalam hal ini, mereka berada 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 event Prefetch .

Mengidentifikasi kemacetan

Secara umum, untuk mengidentifikasi hambatan dalam saluran input Anda, jalankan saluran pipa input dari transformasi terluar sampai ke sumbernya. Mulai dari transformasi akhir di pipeline Anda, rekursi menjadi 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 hulu ke BatchV2 , FiniteRepeat , Map , dan terakhir Range .

Secara umum, transformasi lambat sesuai dengan yang kejadiannya panjang, tetapi kejadian inputnya pendek. Beberapa contoh mengikuti 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 instrumentasi dan autotuning kinerja saluran input.

Jika pekerjaan Anda menggunakan strategi distribusi , penampil pelacakan akan berisi peristiwa tambahan yang sesuai dengan saluran input perangkat. Transformasi terluar dari pipeline perangkat (bersarang di bawah IteratorGetNextOp::DoCompute atau IteratorGetNextAsOptionalOp::DoCompute ) akan menjadi acara Iterator::Prefetch dengan acara Iterator::Generator upstream. Anda dapat menemukan saluran 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()

Pada tangkapan layar, amati bahwa (1) Peristiwa Iterator::Map panjang, tetapi (2) peristiwa inputnya ( Iterator::FlatMap ) kembali dengan cepat. Ini menunjukkan bahwa transformasi Peta sekuensial 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 di atas, tetapi menggunakan ParallelMap bukan Map. Kita perhatikan di sini bahwa (1) peristiwa Iterator::ParallelMap panjang, tetapi (2) peristiwa inputnya Iterator::FlatMap (yang berada di utas yang berbeda, karena ParallelMap tidak sinkron) pendek. Ini menunjukkan bahwa transformasi ParallelMap adalah hambatannya.

Mengatasi kemacetan

Kumpulan data sumber

Jika Anda telah mengidentifikasi sumber kumpulan data sebagai penghambat, seperti membaca dari file TFRecord, Anda dapat meningkatkan kinerja dengan memparalelkan ekstraksi data. Untuk melakukannya, pastikan bahwa data Anda di-sharding ke 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 deterministic=False flag 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 sharding harus cukup besar untuk mengamortisasi overhead pembukaan file. Untuk detail selengkapnya tentang ekstraksi data paralel, lihat bagian panduan kinerja tf.data ini.

Kumpulan data transformasi

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

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 saluran input melalui input yang di-sharding 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