GPU

סקירה כללית

MediaPipe תומך בצמתים של מחשבון למחשוב ועיבוד של GPU, ומאפשר שילוב של מספר צמתים של GPU, וכן ערבוב שלהם עם צמתים של מחשבון המבוססים על המעבד (CPU). יש כמה ממשקי API של GPU בפלטפורמות לנייד (לדוגמה, OpenGL ES, Metal ו-Vulkan). MediaPipe לא מנסה להציע הפשטה יחידה של GPU בכל ממשקי API. אפשר לכתוב צמתים שונים באמצעות ממשקי API שונים, וכך להשתמש בתכונות הספציפיות של הפלטפורמה בעת הצורך.

תמיכה ב-GPU חיונית לביצועים טובים בפלטפורמות ניידות, במיוחד בווידאו בזמן אמת. בעזרת MediaPipe מפתחים יכולים לכתוב מחשבונים תואמים ל-GPU שתומכים בשימוש ב-GPU למטרות הבאות:

  • עיבוד בזמן אמת במכשיר, לא רק עיבוד ברצף (batch processing)
  • עיבוד וידאו ואפקטים, לא רק ניתוח

בהמשך מפורטים עקרונות התכנון לתמיכה ב-GPU ב-MediaPipe

  • מחשבונים המבוססים על GPU אמורים להופיע בכל מקום בתרשים, ולא בהכרח לשמש לעיבוד במסך.
  • ההעברה של נתוני פריימים ממחשבון אחד שמבוסס על GPU למחשבון אחר צריכה להיות מהירה, ובלי לבצע פעולות העתקה יקרות.
  • העברת נתוני הפריימים בין המעבד (CPU) ל-GPU צריכה להיות יעילה ככל שהפלטפורמה מאפשרת.
  • מכיוון שפלטפורמות שונות עשויות לדרוש טכניקות שונות כדי להשיג את הביצועים הטובים ביותר, ה-API צריך לאפשר גמישות באופן שבו הדברים מיושמים מאחורי הקלעים.
  • כדאי להשתמש במחשבון בגמישות מרבית בשימוש ב-GPU לכל פעולתו או בחלקה, ולשלב אותו עם המעבד (CPU) במידת הצורך.

תמיכה ב-Open ES

MediaPipe תומך ב- OpenGL ES עד גרסה 3.2 ב-Android/Linux ועד ES 3.0 ב-iOS. נוסף על כך, MediaPipe תומך גם ב- Metal ב-iOS.

כדי להפעיל גרפים ומחשבי מסקנות ללמידת מכונה, צריך להתקין את OpenGL ES 3.1 ואילך (במערכות Android/Linux).

MediaPipe מאפשר לתרשימים להפעיל את OpenGL בהקשרים מרובים של GL. לדוגמה, זה יכול להיות שימושי מאוד בתרשימים שמשלבים נתיב הסקת מסקנות איטי יותר של ה-GPU (למשל, ב-10 FPS) עם נתיב עיבוד GPU מהיר יותר (למשל, ב-30 FPS): מאחר שהקשר GL אחד תואם לתור פקודות רציף, שימוש באותו הקשר בשתי המשימות יפחית את קצב הפריימים של הרינדור.

אחד האתגרים שבהם MediaPipe משתמש בהקשרים מרובים הוא היכולת לתקשר ביניהם. תרחיש לדוגמה הוא תרחיש עם סרטון קלט שנשלח גם לנתיב של רינדור וגם לנתיב ההסקה, ולעיבוד צריכה להיות גישה לפלט העדכני ביותר שנלמד מההסקה.

אין אפשרות לגשת להקשר OpenGL בכמה שרשורים בו-זמנית. בנוסף, החלפת הקשר ה-GL הפעיל באותו שרשור עלולה להיות איטית במכשירי Android מסוימים. לכן הגישה שלנו היא ליצור שרשור ייעודי אחד לכל הקשר. כל שרשור מנפיק פקודות GL, ויוצר תור פקודות טורי בהקשר שלו, ש לאחר מכן מופעל על ידי ה-GPU באופן אסינכרוני.

חייו של מחשבון GPU

בקטע הזה מוצג המבנה הבסיסי של השיטה Process של מחשבון GPU שנגזר מ-GlSimpleCalculator במחלקת הבסיס. מחשבון ה-GPU LuminanceCalculator מוצג כדוגמה. מתבצעת קריאה לשיטה LuminanceCalculator::GlRender מה-GlSimpleCalculator::Process.

// Converts RGB images into luminance images, still stored in RGB format.
// See GlSimpleCalculator for inputs, outputs and input side packets.
class LuminanceCalculator : public GlSimpleCalculator {
 public:
  absl::Status GlSetup() override;
  absl::Status GlRender(const GlTexture& src,
                        const GlTexture& dst) override;
  absl::Status GlTeardown() override;

 private:
  GLuint program_ = 0;
  GLint frame_;
};
REGISTER_CALCULATOR(LuminanceCalculator);

absl::Status LuminanceCalculator::GlRender(const GlTexture& src,
                                           const GlTexture& dst) {
  static const GLfloat square_vertices[] = {
      -1.0f, -1.0f,  // bottom left
      1.0f,  -1.0f,  // bottom right
      -1.0f, 1.0f,   // top left
      1.0f,  1.0f,   // top right
  };
  static const GLfloat texture_vertices[] = {
      0.0f, 0.0f,  // bottom left
      1.0f, 0.0f,  // bottom right
      0.0f, 1.0f,  // top left
      1.0f, 1.0f,  // top right
  };

  // program
  glUseProgram(program_);
  glUniform1i(frame_, 1);

  // vertex storage
  GLuint vbo[2];
  glGenBuffers(2, vbo);
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

  // vbo 0
  glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
  glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), square_vertices,
               GL_STATIC_DRAW);
  glEnableVertexAttribArray(ATTRIB_VERTEX);
  glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, nullptr);

  // vbo 1
  glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
  glBufferData(GL_ARRAY_BUFFER, 4 * 2 * sizeof(GLfloat), texture_vertices,
               GL_STATIC_DRAW);
  glEnableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
  glVertexAttribPointer(ATTRIB_TEXTURE_POSITION, 2, GL_FLOAT, 0, 0, nullptr);

  // draw
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

  // cleanup
  glDisableVertexAttribArray(ATTRIB_VERTEX);
  glDisableVertexAttribArray(ATTRIB_TEXTURE_POSITION);
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
  glDeleteVertexArrays(1, &vao);
  glDeleteBuffers(2, vbo);

  return absl::OkStatus();
}

עקרונות העיצוב שהוזכרו למעלה הניבו את האפשרויות הבאות לתמיכה ב-MediaPipe GPU:

  • יש לנו סוג נתונים של GPU, שנקרא GpuBuffer, לייצוג נתוני תמונה שעבר אופטימיזציה לשימוש ב-GPU. התוכן המדויק של סוג הנתונים הזה אטום וספציפי לפלטפורמה.
  • API ברמה נמוכה שמבוסס על קומפוזיציה, שבו כל מחשבון שרוצה להשתמש ב-GPU יוצר מכונה של המחלקה GlCalculatorHelper ומחזיק בה. הכיתה הזו מציעה API אגנוסטי פלטפורמה לניהול ההקשר של OpenGL, להגדרת מרקמים עבור קלטים ופלטים וכו'.
  • API ברמה גבוהה שמבוסס על סיווג משנה, שבו מחשבונים פשוטים שמטמיעים את סיווג המשנה של התמונות מ-GlSimpleCalculator וצריכים לבטל רק כמה שיטות וירטואליות בקוד OpenGL הספציפי שלהם, ומחלקת העל תטפל בכל הצנרת.
  • נתונים שצריך לשתף בין כל המחשבונים המבוססים על GPU מסופקים כקלט חיצוני שמוטמע כשירות תרשימים ומנוהל על ידי המחלקה GlCalculatorHelper.
  • השילוב של כלי עזר ספציפיים למחשבון ושירות גרפי משותף מאפשר לנו גמישות רבה בניהול משאב ה-GPU: אנחנו יכולים ליצור הקשר נפרד לכל מחשבון, לשתף הקשר יחיד, להשתמש בנעילה או בעקרונות סנכרון אחרים וכו' - וכל זה מנוהל על ידי המסייע ומוסתר מידע מהמחשבונים הנפרדים.

ממירי GpuBuffer ל-ImageFrame

אנחנו מספקים שני מחשבונים שנקראים GpuBufferToImageFrameCalculator ו-ImageFrameToGpuBufferCalculator. במחשבונים האלה מתבצעת המרה בין ImageFrame ל-GpuBuffer, וכך ניתן ליצור גרפים שמשלבים מחשבון GPU ומעבד (CPU). הן נתמכות גם ב-iOS וגם ב-Android

כשהדבר אפשרי, המחשבונים האלה משתמשים בפונקציונליות ספציפית לפלטפורמה כדי לשתף נתונים בין המעבד (CPU) ל-GPU ללא העתקה.

התרשים הבא מציג את זרימת הנתונים באפליקציה לנייד שלוכדת וידאו מהמצלמה, מריצה אותו בתרשים MediaPipe ומעבדת את הפלט על המסך בזמן אמת. הקו המקווקו מציין אילו חלקים נמצאים בתרשים ה-MediaPipe. האפליקציה הזו מפעילה מסנן לזיהוי קצה Canny במעבד (CPU) באמצעות OpenCV, ומציגה אותו בשכבת-על מעל לסרטון המקורי באמצעות ה-GPU.

הסבר על האינטראקציה בין מחשבונים של GPU

פריימים של וידאו מהמצלמה מוזנים לתרשים כ-GpuBuffer חבילות. אפשר לגשת למקור הקלט באמצעות שני מחשבונים במקביל. GpuBufferToImageFrameCalculator ממירה את המאגר ל-ImageFrame, שנשלח דרך ממיר בגווני אפור ומסנן מתאמים (שניהם מבוססים על OpenCV ופועלים על המעבד (CPU) והפלט מומר שוב ל-GpuBuffer. מחשבון GPU עם מספר קלטים, GlOverlay, מתייחס גם לקלט של GpuBuffer המקורי וגם לזה שיוצא ממזהה הקצה, ומוצג בהם שכבת-על באמצעות תוכנת הצללה. הפלט נשלח חזרה לאפליקציה באמצעות מחשבון קריאה חוזרת (callback), והאפליקציה מעבדת את התמונה למסך באמצעות OpenGL.