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

ملخص

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

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

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 لنا) ، فهذا يعني أن بياناتك متاحة عند طلبها. خط أنابيب الإدخال ليس عنق الزجاجة الخاص بك ؛ راجع دليل Profiler للحصول على مزيد من النصائح العامة لتحليل الأداء.

image

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

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

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

image

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

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

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

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

إذا كان استخدامك منخفضًا ، فهذا يشير إلى أن خط أنابيب الإدخال الخاص بك قد لا يستفيد استفادة كاملة من وحدة المعالجة المركزية المضيفة. يجب عليك الرجوع إلى دليل أداء 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 API (على سبيل المثال ، FiniteRepeat بدلاً من Repeat) ، ولكن يجب أن يكون بديهيًا بما يكفي للتحليل.

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

بالنسبة إلى تحويلات tf.data المتزامنة (مثل Batch and 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 thread. من اسمها الطويل ، يمكنك استنتاج أن 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. على سبيل المثال ، إذا كنت تقرأ من سجلات 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)

مصادر إضافية