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 .
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).
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.
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 IteratorGetNext::DoCompute
.
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 perlu) 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 penelusuran 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.
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.
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, peristiwa 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 upstream 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
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
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 masukannya 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 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 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
- panduan kinerja tf.data tentang cara menulis alur input kinerja
tf.data
- Video di dalam TensorFlow: praktik terbaik
tf.data
- Panduan profiler
- Tutorial profiler dengan colab