נתח את ביצועי tf.data עם TF Profiler

סקירה כללית

מדריך זה מניח היכרות עם TensorFlow Profiler ו- tf.data . מטרתו היא לספק הוראות שלב אחר שלב עם דוגמאות כדי לעזור למשתמשים לאבחן ולתקן בעיות בביצועי צינור הקלט.

כדי להתחיל, אסוף פרופיל של עבודת TensorFlow שלך. הוראות כיצד לעשות זאת זמינות עבור CPUs/GPUs ו- Cloud TPUs .

TensorFlow Trace Viewer

זרימת העבודה של הניתוח המפורטת להלן מתמקדת בכלי מציג המעקב ב-Profiler. כלי זה מציג ציר זמן המציג את משך הפעולות שבוצעו על ידי תוכנית TensorFlow שלך ומאפשר לך לזהות אילו פעולות לוקח הכי הרבה זמן לביצוע. למידע נוסף על מציג המעקב, עיין בסעיף זה במדריך TF Profiler. באופן כללי, אירועי tf.data יופיעו בציר הזמן של המעבד המארח.

זרימת עבודה של ניתוח

אנא עקוב אחר זרימת העבודה למטה. אם יש לך משוב שיעזור לנו לשפר אותו, אנא צור בעיית github עם התווית "comp:data".

1. האם צינור tf.data שלך מייצר נתונים מהיר מספיק?

התחל על ידי בירור אם צינור הקלט הוא צוואר הבקבוק עבור תוכנית TensorFlow שלך.

כדי לעשות זאת, חפש את IteratorGetNext::DoCompute ops במציג המעקב. באופן כללי, אתה מצפה לראות אותם בתחילת שלב. פרוסות אלה מייצגות את הזמן שלוקח לצינור הקלט שלך להניב אצווה של אלמנטים כאשר היא מתבקשת. אם אתה משתמש ב-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 מופעלים על ה-CPU. אתה יכול לבדוק את השימוש שלך עם כלים כמו sar ו- htop , או במסוף הניטור בענן אם אתה פועל על GCP.

אם הניצול שלך נמוך, זה מצביע על כך שצינור הקלט שלך עשוי לא לנצל את מלוא ה-CPU המארח. עליך לעיין במדריך הביצועים של 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 ב-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 של 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 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 במקום במפה. אנו מבחינים כאן כי (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)

משאבים נוספים