GPU

סקירה כללית

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

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

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

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

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

תמיכה ב-OpenGL ES

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

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

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

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

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

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

בקטע הזה מוצג המבנה הבסיסי של שיטת התהליך של 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 על המעבד באמצעות OpenCV, ומציגה אותו בשכבת-על מעל הסרטון המקורי באמצעות ה-GPU.

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

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