Analisis kinerja tf.data dengan TF Profiler

Ringkasan

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

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 jejak di Profiler. Alat ini menampilkan garis waktu yang menunjukkan durasi operasi yang dijalankan oleh program TensorFlow dan memungkinkan Anda mengidentifikasi operasi mana yang paling lama dijalankan. 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 masukan untuk membantu kami memperbaikinya, silakan 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 penghambat program TensorFlow Anda.

Untuk melakukannya, cari operasi IteratorGetNext::DoCompute di penampil jejak. Secara umum, Anda mengharapkan untuk melihatnya di awal sebuah langkah. Irisan ini mewakili waktu yang diperlukan saluran masukan Anda untuk menghasilkan sekumpulan elemen saat diminta. Jika Anda menggunakan keras atau mengulangi kumpulan data Anda dalam tf.function , ini harus ditemukan di thread 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 us), ini berarti data Anda tersedia saat diminta. Saluran masukan bukanlah hambatan Anda; lihat panduan Profiler untuk tips analisis kinerja yang lebih umum.

image

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

2. Apakah Anda mengambil data terlebih dahulu?

Praktik terbaik untuk performa alur input adalah dengan menyisipkan transformasi tf.data.Dataset.prefetch di akhir alur 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 mengambil data terlebih dahulu, Anda akan melihat potongan Iterator::Prefetch di thread yang sama dengan operasi IteratorGetNext::DoCompute .

image

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

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

3. Apakah Anda mencapai pemanfaatan 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 di CPU. Anda dapat memeriksa pemanfaatan Anda dengan alat seperti sar dan htop , atau di konsol pemantauan cloud jika Anda menjalankan GCP.

Jika pemanfaatan Anda rendah, ini menunjukkan bahwa saluran masukan Anda mungkin tidak memanfaatkan CPU host secara penuh. Anda harus membaca panduan kinerja tf.data untuk praktik terbaik. Jika Anda telah menerapkan praktik terbaik dan pemanfaatan serta throughput tetap rendah, lanjutkan ke analisis Hambatan di bawah.

Jika pemanfaatan Anda mendekati batas sumber daya , untuk meningkatkan kinerja lebih lanjut, Anda perlu meningkatkan efisiensi saluran masukan (misalnya, menghindari komputasi yang tidak diperlukan) atau melakukan pembongkaran beban.

Anda dapat meningkatkan efisiensi saluran masukan dengan menghindari komputasi yang tidak perlu di tf.data . Salah satu cara untuk melakukannya adalah dengan memasukkan transformasi tf.data.Dataset.cache setelah pekerjaan intensif komputasi jika data Anda masuk ke dalam memori; ini mengurangi komputasi dengan mengorbankan peningkatan penggunaan memori. Selain itu, menonaktifkan paralelisme intra-operasi di tf.data berpotensi meningkatkan efisiensi sebesar > 10%, dan dapat dilakukan dengan mengatur opsi berikut pada jalur 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 letak 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 acara juga memiliki nama panjang Iterator::<Dataset_1>::...::<Dataset_n> , yang dapat Anda lihat dengan mengklik acara 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, bukan Repeat), tetapi harus cukup intuitif untuk diurai.

Transformasi sinkron dan asinkron

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

Untuk transformasi asinkron (seperti Prefetch , ParallelMap , ParallelInterleave dan MapAndBatch ) kejadian dari transformasi upstream akan berada di thread yang berbeda. Dalam kasus seperti itu, “nama panjang” dapat membantu Anda mengidentifikasi transformasi mana dalam pipeline yang sesuai dengan suatu 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 thread tf_data_iterator_get_next . Karena Prefetch tidak sinkron, kejadian inputnya ( BatchV2 ) akan berada di thread yang berbeda, dan dapat ditemukan dengan mencari nama panjang Iterator::Prefetch::BatchV2 . Dalam hal ini, mereka berada di thread tf_data_iterator_resource . Dari namanya yang panjang, Anda dapat menyimpulkan bahwa BatchV2 adalah upstream dari Prefetch . Selanjutnya, parent_id acara BatchV2 akan cocok dengan ID acara Prefetch .

Mengidentifikasi hambatan

Secara umum, untuk mengidentifikasi hambatan dalam saluran masukan Anda, jalankan saluran masukan dari transformasi terluar hingga ke sumbernya. Mulai dari transformasi akhir di alur Anda, ulangi ke transformasi hulu hingga Anda menemukan transformasi yang lambat atau mencapai kumpulan data sumber, seperti TFRecord . Pada contoh di atas, Anda akan memulai dari Prefetch , lalu berjalan ke hulu ke BatchV2 , FiniteRepeat , Map , dan terakhir Range .

Secara umum, transformasi yang lambat berhubungan dengan transformasi yang kejadiannya panjang, tetapi kejadian masukannya pendek. Beberapa contoh ikuti di bawah ini.

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

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

Contoh 1

image

Tangkapan layar di atas dihasilkan dari pipa masukan 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 masukannya ( Iterator::FlatMap ) kembali dengan cepat. Hal ini menunjukkan bahwa transformasi Peta sekuensial adalah hambatannya.

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

Contoh 2

image

Tangkapan layar di atas dihasilkan dari pipa masukan berikut:

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

Contoh ini serupa dengan contoh di atas, namun menggunakan ParallelMap dan bukan Map. Kita perhatikan di sini bahwa (1) peristiwa Iterator::ParallelMap panjang, tetapi (2) peristiwa inputnya Iterator::FlatMap (yang berada di thread berbeda, karena ParallelMap tidak sinkron) pendek. Hal ini menunjukkan bahwa transformasi ParallelMap adalah hambatannya.

Mengatasi kemacetan

Kumpulan data sumber

Jika Anda telah mengidentifikasi sumber himpunan data sebagai hambatan, seperti membaca dari file TFRecord, Anda dapat meningkatkan kinerja dengan memparalelkan ekstraksi data. Untuk melakukannya, pastikan data Anda dibagi ke beberapa file dan gunakan tf.data.Dataset.interleave dengan parameter num_parallel_calls disetel ke tf.data.AUTOTUNE . Jika determinisme tidak penting bagi program Anda, Anda dapat meningkatkan kinerja lebih lanjut dengan menyetel tanda deterministic=False pada tf.data.Dataset.interleave mulai 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 yang dipecah harus berukuran cukup besar untuk mengamortisasi biaya overhead pembukaan file. Untuk detail selengkapnya 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 dalam cache jika data Anda masuk ke dalam memori dan sesuai. Beberapa transformasi seperti Map memiliki padanan paralel; panduan kinerja tf.data menunjukkan cara memparalelkannya. Transformasi lainnya, seperti Filter , Unbatch , dan Batch pada dasarnya bersifat berurutan; Anda dapat memparalelkannya dengan memperkenalkan “paralelisme luar”. Misalnya, saluran masukan 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 beberapa salinan saluran masukan melalui masukan 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