ML Community Day คือวันที่ 9 พฤศจิกายน! ร่วมกับเราสำหรับการปรับปรุงจาก TensorFlow, JAX และอื่น ๆ เรียนรู้เพิ่มเติม

วิเคราะห์ประสิทธิภาพ tf.data ด้วย TF Profiler

ภาพรวม

คู่มือนี้ถือว่าคุ้นเคยกับ TensorFlow Profiler และ tf.data โดยมีจุดมุ่งหมายเพื่อให้คำแนะนำทีละขั้นตอนพร้อมตัวอย่างเพื่อช่วยผู้ใช้ในการวินิจฉัยและแก้ไขปัญหาประสิทธิภาพของท่อส่งข้อมูลเข้า

ในการเริ่มต้นรวบรวมโปรไฟล์ของงาน TensorFlow ของคุณ มีคำแนะนำในการดำเนินการสำหรับ CPU / GPU และ Cloud TPU

TensorFlow Trace Viewer

เวิร์กโฟลว์การวิเคราะห์ที่มีรายละเอียดด้านล่างนี้มุ่งเน้นไปที่เครื่องมือติดตามวิวใน Profiler เครื่องมือนี้แสดงไทม์ไลน์ที่แสดงระยะเวลาของการดำเนินการโดยโปรแกรม TensorFlow ของคุณและช่วยให้คุณระบุได้ว่าหน่วยปฏิบัติการใดใช้เวลาดำเนินการนานที่สุด สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิวเวอร์การติดตามโปรดดู ส่วนนี้ ของคู่มือ TF Profiler โดยทั่วไปเหตุการณ์ tf.data จะปรากฏบนไทม์ไลน์ของโฮสต์ CPU

เวิร์กโฟลว์การวิเคราะห์

โปรดปฏิบัติตามขั้นตอนการทำงานด้านล่าง หากคุณมีข้อเสนอแนะเพื่อช่วยเราปรับปรุงโปรด สร้างปัญหาเกี่ยว กับ GitHub โดยใช้ป้ายกำกับ“ comp: data”

1. ไปป์ไลน์ tf.data ของคุณผลิตข้อมูลเร็วพอหรือไม่?

เริ่มต้นด้วยการตรวจสอบว่าไปป์ไลน์อินพุตเป็นคอขวดสำหรับโปรแกรม TensorFlow ของคุณหรือไม่

ในการทำเช่นนั้นให้มองหา IteratorGetNext::DoCompute ops ใน trace viewer โดยทั่วไปคุณคาดว่าจะเห็นสิ่งเหล่านี้เมื่อเริ่มขั้นตอน ชิ้นส่วนเหล่านี้แสดงถึงเวลาที่ใช้ในการป้อนข้อมูลไปป์ไลน์ของคุณเพื่อให้ได้ชุดขององค์ประกอบเมื่อมีการร้องขอ หากคุณกำลังใช้ keras หรือทำซ้ำชุดข้อมูลของคุณใน tf.function สิ่งเหล่านี้ควรอยู่ในเธรด tf_data_iterator_get_next

โปรดทราบว่าหากคุณใช้ กลยุทธ์การแจกจ่าย คุณอาจเห็นเหตุการณ์ IteratorGetNextAsOptional::DoCompute แทน IteratorGetNext::DoCompute (ณ TF 2.3)

image

หากการโทรกลับมาอย่างรวดเร็ว (<= 50 us) หมายความว่าข้อมูลของคุณจะพร้อมใช้งานเมื่อมีการร้องขอ ท่อส่งข้อมูลไม่ใช่คอขวดของคุณ ดู คู่มือ Profiler สำหรับเคล็ดลับการวิเคราะห์ประสิทธิภาพทั่วไปเพิ่มเติม

image

หากการโทรกลับช้า tf.data จะไม่สามารถทำตามคำขอของผู้บริโภคได้ ดำเนินการต่อในหัวข้อถัดไป

2. คุณกำลังดึงข้อมูลล่วงหน้าหรือไม่?

แนวทางปฏิบัติที่ดีที่สุดสำหรับประสิทธิภาพของไปป์ไลน์อินพุตคือการแทรกการแปลง tf.data.Dataset.prefetch ที่ส่วนท้ายของไปป์ไลน์ tf.data ของคุณ การแปลงนี้ซ้อนทับการคำนวณก่อนการประมวลผลของไปป์ไลน์อินพุตกับขั้นตอนถัดไปของการคำนวณโมเดลและจำเป็นสำหรับประสิทธิภาพของไปป์ไลน์อินพุตที่เหมาะสมที่สุดเมื่อฝึกโมเดลของคุณ หากคุณกำลังดึงข้อมูลล่วงหน้าคุณควรเห็น Iterator::Prefetch slice บนเธรดเดียวกับ IteratorGetNext::DoCompute op

image

หากคุณไม่มีการ prefetch ที่ส่วนท้ายของไปป์ไลน์ คุณควรเพิ่มเข้าไป สำหรับข้อมูลเพิ่มเติมเกี่ยวกับคำแนะนำประสิทธิภาพ tf.data โปรดดู คู่มือประสิทธิภาพ tf.data

หากคุณกำลังดึงข้อมูลไว้ล่วงหน้าแล้ว และท่อส่งข้อมูลยังคงเป็นคอขวดของคุณให้เข้าสู่ส่วนถัดไปเพื่อวิเคราะห์ประสิทธิภาพเพิ่มเติม

3. คุณมีการใช้งาน CPU สูงหรือไม่?

tf.data สูงโดยพยายามใช้ทรัพยากรที่มีอยู่ให้เกิดประโยชน์สูงสุด โดยทั่วไปแม้ว่าจะเรียกใช้โมเดลของคุณบนเครื่องเร่งความเร็วเช่น GPU หรือ TPU ไปป์ไลน์ tf.data จะทำงานบน CPU คุณสามารถตรวจสอบการใช้งานของคุณด้วยเครื่องมือเช่น sar และ htop หรือใน คอนโซลการตรวจสอบระบบคลาวด์ หากคุณใช้งาน GCP

หากการใช้งานของคุณต่ำ แสดงว่าไปป์ไลน์อินพุตของคุณอาจใช้ประโยชน์จากโฮสต์ CPU ไม่เต็มที่ คุณควรอ่าน คู่มือประสิทธิภาพ tf.data สำหรับแนวทางปฏิบัติที่ดีที่สุด หากคุณใช้แนวทางปฏิบัติที่ดีที่สุดและการใช้ประโยชน์และปริมาณงานยังคงต่ำให้ไปที่ การวิเคราะห์ Bottleneck ด้านล่าง

หากการใช้งานของคุณใกล้ถึงขีด จำกัด ทรัพยากร เพื่อปรับปรุงประสิทธิภาพให้ดียิ่งขึ้นคุณจำเป็นต้องปรับปรุงประสิทธิภาพของท่อส่งข้อมูลของคุณ (เช่นหลีกเลี่ยงการคำนวณที่ไม่จำเป็น) หรือการคำนวณแบบ offload

คุณสามารถปรับปรุงประสิทธิภาพของไปป์ไลน์อินพุตของคุณได้โดยหลีกเลี่ยงการคำนวณที่ไม่จำเป็นใน tf.data วิธีหนึ่งในการทำเช่นนี้คือการแทรกการแปลง tf.data.Dataset.cache หลังจากทำงานที่ต้องใช้การคำนวณเป็นจำนวนมากหากข้อมูลของคุณพอดีกับหน่วยความจำ ซึ่งจะช่วยลดการคำนวณด้วยต้นทุนของการใช้หน่วยความจำที่เพิ่มขึ้น นอกจากนี้การปิดใช้งาน intra-op parallelism ใน 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 ใน Profiler

แต่ละเหตุการณ์ tf.data ใน Profiler มีชื่อ 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 แทนการทำซ้ำ) แต่ควรใช้งานง่ายพอที่จะแยกวิเคราะห์

การแปลงแบบซิงโครนัสและอะซิงโครนัส

สำหรับการแปลง tf.data ซิงโครนัส (เช่น Batch และ Map ) คุณจะเห็นเหตุการณ์จากการแปลงต้นน้ำบนเธรดเดียวกัน ในตัวอย่างข้างต้นเนื่องจากการแปลงทั้งหมดที่ใช้เป็นแบบซิงโครนัสเหตุการณ์ทั้งหมดจึงปรากฏในเธรดเดียวกัน

สำหรับการแปลงแบบอะซิงโครนัส (เช่น Prefetch , ParallelMap , ParallelInterleave และ MapAndBatch ) เหตุการณ์จากการแปลงต้นน้ำจะอยู่บนเธรดอื่น ในกรณีเช่นนี้“ ชื่อยาว” สามารถช่วยให้คุณระบุได้ว่าการเปลี่ยนแปลงใดในไปป์ไลน์ที่เหตุการณ์สอดคล้องกับ

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 จากชื่อที่ยาวคุณสามารถอนุมานได้ว่า BatchV2 น้ำของ Prefetch นอกจากนี้ parent_id ของเหตุการณ์ BatchV2 จะตรงกับ ID ของเหตุการณ์ Prefetch

การระบุคอขวด

โดยทั่วไปในการระบุคอขวดในท่อป้อนข้อมูลของคุณให้เดินท่อนำเข้าจากการแปลงด้านนอกสุดไปจนถึงแหล่งที่มา เริ่มต้นจากการเปลี่ยนแปลงขั้นสุดท้ายในไปป์ไลน์ของคุณเก็บรวบรวมเป็นการแปลงต้นน้ำจนกว่าคุณจะพบการเปลี่ยนแปลงที่ช้าหรือเข้าถึงชุดข้อมูลต้นทางเช่น TFRecord ในตัวอย่างข้างต้นคุณจะเริ่มจาก Prefetch จากนั้นเดินทวนน้ำไปที่ BatchV2 , FiniteRepeat , Map และสุดท้ายคือ Range

โดยทั่วไปการเปลี่ยนแปลงอย่างช้าๆจะสอดคล้องกับเหตุการณ์ที่มีความยาว แต่เหตุการณ์ที่ป้อนเข้านั้นสั้น ตัวอย่างบางส่วนทำตามด้านล่าง

โปรดสังเกตว่าการแปลงขั้นสุดท้าย (ด้านนอกสุด) ในไปป์ไลน์อินพุตโฮสต์ส่วนใหญ่คือเหตุการณ์ Iterator::Model การแปลงแบบจำลองถูกนำมาใช้โดยอัตโนมัติโดยรันไทม์ tf.data และใช้สำหรับการวัดและปรับแต่งประสิทธิภาพของท่อส่งข้อมูลเข้าโดยอัตโนมัติ

หากงานของคุณใช้ กลยุทธ์การกระจายตัว แสดงการติดตามจะมีเหตุการณ์เพิ่มเติมที่สอดคล้องกับไปป์ไลน์อินพุตของอุปกรณ์ การเปลี่ยนแปลงด้านนอกสุดของไปป์ไลน์อุปกรณ์ (ซ้อนอยู่ภายใต้ IteratorGetNextOp::DoCompute หรือ IteratorGetNextAsOptionalOp::DoCompute ) จะเป็นเหตุการณ์ Iterator::Prefetch มีเหตุการณ์ Iterator::Generator คุณสามารถค้นหาไปป์ไลน์โฮสต์ที่เกี่ยวข้องได้โดยค้นหา Iterator::Model events

ตัวอย่าง 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 ) จะกลับมาอย่างรวดเร็ว สิ่งนี้ชี้ให้เห็นว่าการเปลี่ยนแปลงแผนที่ตามลำดับเป็นปัญหาคอขวด

โปรดทราบว่าในภาพหน้าจอเหตุการณ์ InstantiatedCapturedFunction::Run จะสอดคล้องกับเวลาที่ใช้ในการเรียกใช้ฟังก์ชันแผนที่

ตัวอย่างที่ 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 คุณสามารถปรับปรุงประสิทธิภาพได้โดยการแยกข้อมูลแบบขนาน ต้องการทำเช่นนั้นให้มั่นใจว่าข้อมูลของคุณจะ sharded ในหลายไฟล์และใช้ tf.data.Dataset.interleave กับ num_parallel_calls พารามิเตอร์ชุด tf.data.AUTOTUNE หากดี tf.data.Dataset.interleave ไม่สำคัญต่อโปรแกรมของคุณคุณสามารถปรับปรุงประสิทธิภาพเพิ่มเติมได้โดยตั้งค่าแฟ tf.data.Dataset.interleave deterministic=False บน tf.data.Dataset.interleave ณ TF 2.2 ตัวอย่างเช่นหากคุณกำลังอ่านจาก TFRecords คุณสามารถทำสิ่งต่อไปนี้:

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

โปรดทราบว่าไฟล์ที่ชาร์ดควรมีขนาดใหญ่พอสมควรเพื่อตัดค่าใช้จ่ายในการเปิดไฟล์ สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับการแยกข้อมูลแบบขนานโปรดดู ส่วนนี้ ของคู่มือประสิทธิภาพ tf.data

ชุดข้อมูลการเปลี่ยนแปลง

หากคุณระบุว่าการแปลง tf.data ระดับกลางเป็นคอขวดคุณสามารถจัดการได้โดยการขนานการแปลงหรือ แคชการคำนวณ หากข้อมูลของคุณพอดีกับหน่วยความจำและเหมาะสม การเปลี่ยนแปลงบางอย่างเช่น Map มีคู่ขนานกัน คู่มือประสิทธิภาพ tf.data แสดงให้เห็นถึง วิธีการขนานสิ่งเหล่านี้ การเปลี่ยนแปลงอื่น ๆ เช่น Filter , Unbatch และ Batch จะเป็นไปตามลำดับโดยเนื้อแท้ คุณสามารถทำให้ขนานกันได้โดยการแนะนำ "การขนานภายนอก" ตัวอย่างเช่นสมมติว่าไปป์ไลน์อินพุตของคุณในตอนแรกมีลักษณะดังต่อไปนี้โดยมี 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.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)

แหล่งข้อมูลเพิ่มเติม