قم بتحليل أداء tf.data باستخدام ملف تعريف TF

ملخص

يفترض هذا الدليل الإلمام بملف تعريف TensorFlow و tf.data . ويهدف إلى توفير إرشادات خطوة بخطوة مع أمثلة لمساعدة المستخدمين على تشخيص وإصلاح مشكلات أداء خط أنابيب الإدخال.

للبدء، قم بتجميع ملف تعريف لوظيفة TensorFlow الخاصة بك. تتوفر إرشادات حول كيفية القيام بذلك لوحدات المعالجة المركزية/وحدات معالجة الرسومات ووحدات TPU السحابية .

TensorFlow Trace Viewer

يركز سير عمل التحليل المفصل أدناه على أداة عارض التتبع في ملف التعريف. تعرض هذه الأداة مخططًا زمنيًا يوضح مدة العمليات التي ينفذها برنامج TensorFlow الخاص بك وتسمح لك بتحديد العمليات التي تستغرق وقتًا أطول للتنفيذ. لمزيد من المعلومات حول عارض التتبع، راجع هذا القسم من دليل TF Profiler. بشكل عام، ستظهر أحداث tf.data على المخطط الزمني لوحدة المعالجة المركزية المضيفة.

سير عمل التحليل

يرجى اتباع سير العمل أدناه. إذا كانت لديك تعليقات لمساعدتنا في تحسينها، فيرجى إنشاء مشكلة في github تحمل التصنيف "comp:data".

1. هل يقوم خط أنابيب tf.data الخاص بك بإنتاج البيانات بالسرعة الكافية؟

ابدأ بالتأكد مما إذا كان خط أنابيب الإدخال هو عنق الزجاجة لبرنامج TensorFlow الخاص بك.

للقيام بذلك، ابحث عن عمليات IteratorGetNext::DoCompute في عارض التتبع. بشكل عام، تتوقع رؤية هذه العناصر في بداية الخطوة. تمثل هذه الشرائح الوقت الذي يستغرقه مسار الإدخال الخاص بك لإنتاج مجموعة من العناصر عند طلبها. إذا كنت تستخدم keras أو تتكرر على مجموعة البيانات الخاصة بك في tf.function ، فيجب العثور عليها في سلاسل الرسائل tf_data_iterator_get_next .

لاحظ أنه إذا كنت تستخدم إستراتيجية توزيع ، فقد ترى أحداث IteratorGetNextAsOptional::DoCompute بدلاً من IteratorGetNext::DoCompute (اعتبارًا من TF 2.3).

image

إذا عادت المكالمات بسرعة (<= 50 لنا)، فهذا يعني أن بياناتك متاحة عند طلبها. خط أنابيب الإدخال ليس عنق الزجاجة الخاص بك؛ راجع دليل ملف التعريف للحصول على نصائح أكثر عمومية لتحليل الأداء.

image

إذا عادت المكالمات ببطء، فلن يتمكن tf.data من مواكبة طلبات المستهلك. انتقل إلى القسم التالي.

2. هل تقوم بجلب البيانات مسبقًا؟

أفضل الممارسات لأداء مسار الإدخال هي إدراج تحويل tf.data.Dataset.prefetch في نهاية مسار tf.data الخاص بك. يتداخل هذا التحويل مع حساب المعالجة المسبقة لخط أنابيب الإدخال مع الخطوة التالية لحساب النموذج وهو مطلوب لتحقيق الأداء الأمثل لخط أنابيب الإدخال عند تدريب النموذج الخاص بك. إذا كنت تقوم بالجلب المسبق للبيانات، فيجب أن تشاهد شريحة Iterator::Prefetch على نفس مؤشر الترابط مثل IteratorGetNext::DoCompute .

image

إذا لم يكن لديك prefetch في نهاية المسار ، فيجب عليك إضافة واحد. لمزيد من المعلومات حول توصيات أداء tf.data ، راجع دليل أداء tf.data .

إذا كنت تقوم بالفعل بالجلب المسبق للبيانات ، ولا يزال مسار الإدخال يمثل عنق الزجاجة لديك، فانتقل إلى القسم التالي لمزيد من تحليل الأداء.

3. هل وصلت إلى معدل استخدام عالٍ لوحدة المعالجة المركزية؟

يحقق tf.data إنتاجية عالية من خلال محاولة تحقيق أفضل استخدام ممكن للموارد المتاحة. بشكل عام، حتى عند تشغيل النموذج الخاص بك على مسرع مثل GPU أو TPU، يتم تشغيل مسارات tf.data على وحدة المعالجة المركزية (CPU). يمكنك التحقق من استخدامك باستخدام أدوات مثل sar و htop ، أو في وحدة التحكم في المراقبة السحابية إذا كنت تستخدم Google Cloud Platform.

إذا كان استخدامك منخفضًا، فهذا يشير إلى أن خط الإدخال الخاص بك قد لا يستفيد بشكل كامل من وحدة المعالجة المركزية المضيفة. يجب عليك الرجوع إلى دليل أداء tf.data للحصول على أفضل الممارسات. إذا قمت بتطبيق أفضل الممارسات وظل الاستخدام والإنتاجية منخفضين، فتابع إلى تحليل الاختناق أدناه.

إذا كان استخدامك يقترب من حد الموارد ، فمن أجل تحسين الأداء بشكل أكبر، تحتاج إما إلى تحسين كفاءة خط أنابيب الإدخال الخاص بك (على سبيل المثال، تجنب الحسابات غير الضرورية) أو إلغاء التحميل.

يمكنك تحسين كفاءة مسار الإدخال الخاص بك عن طريق تجنب الحسابات غير الضرورية في tf.data . إحدى طرق القيام بذلك هي إدخال تحويل tf.data.Dataset.cache بعد العمل الذي يتطلب عمليات حسابية مكثفة إذا كانت بياناتك مناسبة للذاكرة؛ وهذا يقلل من العمليات الحسابية على حساب زيادة استخدام الذاكرة. بالإضافة إلى ذلك، فإن تعطيل التوازي أثناء العملية في 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 في ملف التعريف

كل حدث tf.data في ملف التعريف له اسم 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 (على سبيل المثال، FiniteRepeat بدلاً من Repeat)، ولكن يجب أن يكون بديهيًا بدرجة كافية للتحليل.

التحولات المتزامنة وغير المتزامنة

بالنسبة لتحويلات 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 مع معرف حدث Prefetch .

تحديد عنق الزجاجة

بشكل عام، لتحديد عنق الزجاجة في مسار الإدخال الخاص بك، قم بالسير في مسار الإدخال من التحويل الأبعد إلى المصدر. بدءًا من التحويل النهائي في المسار الخاص بك، قم بالتكرار إلى التحويلات الأولية حتى تجد تحويلًا بطيئًا أو تصل إلى مجموعة بيانات المصدر، مثل TFRecord . في المثال أعلاه، ستبدأ من Prefetch ، ثم تنتقل إلى BatchV2 ، و FiniteRepeat ، و Map ، وأخيرًا Range .

بشكل عام، يتوافق التحول البطيء مع الشخص الذي تكون أحداثه طويلة، ولكن أحداثه المدخلة قصيرة. بعض الأمثلة تتبع أدناه.

لاحظ أن التحويل النهائي (الأبعد) في معظم خطوط أنابيب إدخال المضيف هو حدث Iterator::Model . يتم تقديم تحويل النموذج تلقائيًا من خلال وقت تشغيل tf.data ويتم استخدامه لقياس أداء خط أنابيب الإدخال وضبطه تلقائيًا.

إذا كانت مهمتك تستخدم إستراتيجية توزيع ، فسيحتوي عارض التتبع على أحداث إضافية تتوافق مع مسار إدخال الجهاز. سيكون التحويل الخارجي لخط أنابيب الجهاز (المتداخل ضمن IteratorGetNextOp::DoCompute أو IteratorGetNextAsOptionalOp::DoCompute ) حدث Iterator::Prefetch مع حدث Iterator::Generator الرئيسي. يمكنك العثور على مسار المضيف المقابل من خلال البحث عن أحداث Iterator::Model .

مثال 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، فيمكنك تحسين الأداء عن طريق موازنة استخراج البيانات. للقيام بذلك، تأكد من تقسيم بياناتك عبر ملفات متعددة واستخدم tf.data.Dataset.interleave مع تعيين المعلمة num_parallel_calls على tf.data.AUTOTUNE . إذا لم تكن الحتمية مهمة لبرنامجك، فيمكنك تحسين الأداء بشكل أكبر عن طريق تعيين العلامة deterministic=False على tf.data.Dataset.interleave اعتبارًا من TF 2.2. على سبيل المثال، إذا كنت تقرأ من TRecords، فيمكنك القيام بما يلي:

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)

موارد إضافية