عملکرد 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 در نمایشگر ردیابی بگردید. به طور کلی، شما انتظار دارید که اینها را در ابتدای یک مرحله ببینید. این برش‌ها نشان‌دهنده زمانی است که خط لوله ورودی شما در صورت درخواست، دسته‌ای از عناصر را تولید می‌کند. اگر از 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. آیا به استفاده از CPU بالا رسیده اید؟

tf.data با تلاش برای استفاده بهینه از منابع موجود به توان عملیاتی بالایی دست می یابد. به طور کلی، حتی زمانی که مدل خود را روی یک شتاب دهنده مانند GPU یا TPU اجرا می کنید، خطوط لوله tf.data روی CPU اجرا می شوند. می‌توانید میزان استفاده خود را با ابزارهایی مانند sar و htop یا در کنسول مانیتورینگ ابری اگر روی GCP کار می‌کنید، بررسی کنید.

اگر استفاده شما کم است، این نشان می دهد که خط لوله ورودی شما ممکن است از CPU میزبان بهره کامل نبرد. برای بهترین شیوه ها باید به راهنمای عملکرد tf.data مراجعه کنید. اگر بهترین روش‌ها را اعمال کرده‌اید و بهره‌برداری و توان عملیاتی کم است، به تحلیل Bottleneck زیر ادامه دهید.

اگر استفاده شما به محدودیت منابع نزدیک می شود ، برای بهبود بیشتر عملکرد، باید کارایی خط لوله ورودی خود را بهبود بخشید (برای مثال، اجتناب از محاسبات غیر ضروری) یا محاسبات تخلیه بار.

با اجتناب از محاسبات غیر ضروری در 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 در 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 است. توجه داشته باشید که نام مجموعه داده‌ها ممکن است کمی با API پایتون متفاوت باشد (به عنوان مثال، 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()

این مثال مشابه مثال بالا است، اما به جای Map از ParallelMap استفاده می کند. در اینجا متوجه می‌شویم که (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)

منابع اضافی