ניהול זכרון מהמארח ב-CUDA - כשה-CPU וה-GPU צריכים לדבר¶
עד עכשיו רוב הפוסטים התמקדו במה שקורה בתוך ה-GPU עצמו. אבל לפני שנתונים בכלל מגיעים ל-GPU, הם צריכים לצאת מהמארח - ה-CPU וזיכרון המחשב הרגיל - ולעבור אליו. הניהול של המעבר הזה הוא חלק קריטי מכתיבת קוד CUDA, ולפעמים דווקא הוא, ולא החישוב עצמו, הופך לצוואר הבקבוק של האפליקציה כולה.
הדרך הבסיסית - הקצאה והעתקה מפורשת¶
הצורה הכי בסיסית של עבודה מול הזיכרון היא להקצות זיכרון בנפרד בשני הצדדים - זיכרון רגיל בצד ה-CPU, וזיכרון ייעודי על ה-GPU דרך פונקציה שמקצה שטח על החומרה. אחרי שיש לכם שני אזורי זיכרון נפרדים, אתם מעתיקים נתונים במפורש מהמארח אל ה-GPU, מריצים את ה-kernel, ואז מעתיקים את התוצאה בחזרה מה-GPU אל המארח כדי לקרוא אותה.
הגישה הזו ברורה ופשוטה להבנה, אבל יש לה מחיר - כל העתקה כזו לוקחת זמן, וזמן ההעתקה תלוי בפס הרוחב של החיבור בין ה-CPU ל-GPU, שלרוב איטי משמעותית מפס הרוחב הפנימי בתוך ה-GPU עצמו. אם אתם מעתיקים כמויות גדולות של נתונים הלוך ושוב, ולא נזהרים, ההעתקות האלה יכולות לקחת יותר זמן מהחישוב עצמו.
Pinned Memory - זיכרון שלא זז¶
זיכרון רגיל שהמערכת הפעלה מקצה יכול, מבחינה טכנית, "לזוז" בזיכרון הפיזי במהלך הריצה - מנגנון ניהול הזיכרון של המערכת יכול להעביר אותו לדיסק ובחזרה, או לשנות את מיקומו. הבעיה היא שהעתקה ישירה בין המארח ל-GPU דורשת שהזיכרון יישאר במקום קבוע לאורך כל ההעתקה. כשהזיכרון הוא רגיל, הדרייבר צריך קודם להעתיק אותו לאזור זמני קבוע, ורק משם להעביר אותו הלאה ל-GPU - שלב נוסף שמוסיף זמן.
הפתרון הוא pinned memory, זיכרון מארח שמוקצה בצורה מיוחדת ומובטח שלא "יזוז" במהלך הריצה. העתקות מ-pinned memory ל-GPU מהירות יותר משמעותית, ומאפשרות גם העברות אסינכרוניות - כלומר, אפשר להתחיל העתקת נתונים ובמקביל להמשיך לעבוד על ה-CPU או אפילו להריץ kernel אחר על ה-GPU, במקום לחכות שההעתקה תסתיים לפני שממשיכים הלאה.
Unified Memory - הגישה הפשוטה יותר¶
לצד הגישה הידנית והמדויקת, CUDA מציעה גם Unified Memory - שכבת הפשטה שבה אתם מקצים אזור זיכרון אחד, וגם ה-CPU וגם ה-GPU יכולים לגשת אליו ישירות, בלי שאתם צריכים לכתוב העתקות מפורשות בעצמכם. מאחורי הקלעים, המערכת מזיזה את הנתונים בין המארח ל-GPU בהתאם למי שניגש אליהם באותו רגע.
היתרון הברור הוא פשטות - קוד קצר יותר, פחות מקום לטעויות. החיסרון הוא פחות שליטה - אתם לא תמיד יודעים בדיוק מתי ההעברות קורות, ובמקרים מסוימים זה יכול להיות פחות יעיל מהעתקה מפורשת ומתוזמנת היטב. ברוב הפרויקטים הגדולים, במיוחד כאלה שרודפים אחרי כל אחוז ביצועים, עדיין נהוג להשתמש בניהול מפורש עם pinned memory. אבל בשביל למידה, אבטיפוס מהיר, או קוד שלא רגיש עד כדי כך לביצועים, Unified Memory יכולה לחסוך המון זמן פיתוח.
איך זה משתלב בתמונה הגדולה¶
ניהול זיכרון מהמארח הוא לרוב הצעד הראשון שכל תוכנית CUDA עוברת, עוד לפני ה-kernel עצמו, וגם הצעד האחרון, כשהתוצאה חוזרת בחזרה. הבנה נכונה שלו, במיוחד בפרויקטים שמעבירים נתונים הלוך ושוב שוב ושוב, יכולה לחסוך זמן ריצה משמעותי בלי לגעת בכלל בלוגיקת החישוב עצמה.
זה בדיוק אחד הנושאים הראשונים שאנחנו מכסים בקורס תכנות GPU, כי בלי להבין איך נתונים זזים בין המארח לGPU, קשה להבין למה קוד שנראה תקין רץ לאט יותר ממה שציפיתם.
לסיכום¶
ניהול זיכרון בין המארח לGPU הוא הרבה יותר מפרט טכני שממלאים באמצע כתיבת קוד. הבחירה בין העתקה מפורשת, pinned memory, ו-Unified Memory יכולה להיות ההבדל בין תוכנית שמנצלת את ה-GPU בצורה יעילה, לתוכנית שמבזבזת רוב זמן הריצה שלה על העברת נתונים הלוך ושוב.
בואו תלמדו לנהל את הקצה הזה נכון, עם קהילה שכבר שם.