מה זה Launch Configuration ב-CUDA - איך קובעים כמה threads להריץ¶
כשמשגרים kernel ב-CUDA, לא מספיק לכתוב מה כל thread צריך לעשות. צריך גם להגיד לחומרה בדיוק כמה threads אתם רוצים, ואיך הם מאורגנים. ההחלטה הזו נקראת launch configuration, וזו אחת ההחלטות הראשונות שכל מתכנת CUDA לומד לקבל, ואחת שממשיכה להשפיע על ביצועים עד הפרויקטים המתקדמים ביותר.
grid, block ו-thread בקצרה¶
כפי שכבר ראינו כשדיברנו על kernel ב-CUDA, threads מאורגנים בהיררכיה - קבוצות של threads נקראות block, וכל ה-blocks יחד מרכיבים grid. כשאתם משגרים kernel, אתם קובעים שני מספרים - כמה threads יש בכל block, וכמה blocks יש בכל grid. השילוב של השניים הוא ה-launch configuration.
בתחביר בפועל זה נראה כמו הוספת שני ערכים בסוגריים משולשים לפני קריאת הפונקציה - אחד לגודל ה-block, אחד לגודל ה-grid. שני הערכים האלה יכולים להיות מספר בודד, או מבנה תלת-ממדי, תלוי אם הנתונים שלכם הם וקטור, מטריצה, או משהו תלת-ממדי כמו נפח תמונות.
למה גודל ה-block לא שרירותי¶
הבחירה הראשונה שצריך לקבל היא כמה threads יהיו בכל block. אפשר לחשוב שזה עניין שרירותי, אבל יש לזה השלכות ממשיות. threads בתוך block רצים על אותו SM, וכל 32 מהם מאורגנים יחד בתוך warp אחד, כפי שדיברנו בפוסט על warp divergence. אם גודל ה-block לא מתחלק יפה ב-32, חלק מכל warp אחרון פשוט "מבוזבז" על threads לא פעילים.
מעבר לזה, גודל ה-block משפיע ישירות על occupancy - כמה blocks יכולים לרוץ במקביל על אותו SM תלוי כמה משאבים כל block צורך. block גדול מדי עלול להגביל כמה blocks נכנסים יחד לאותו SM, ו-block קטן מדי עלול לא לנצל מספיק את המשאבים הפנויים. בפועל, גדלים נפוצים כמו 128, 256 או 512 threads לכל block הם נקודת התחלה סבירה, אבל התשובה הנכונה תלויה בקוד הספציפי ובחומרה.
איך מחשבים גודל grid שמכסה הכל¶
ברוב המקרים, גודל הנתונים שלכם לא מתחלק בול בגודל ה-block שבחרתם. אם יש לכם מיליון איברים ואתם משתמשים ב-block בגודל 256, מיליון חלקי 256 לא יוצא מספר שלם. הפתרון הנפוץ הוא לעגל את מספר ה-blocks כלפי מעלה, כך שהם יחד מכסים לפחות את כל הנתונים, ולוודא בתוך ה-kernel עצמו שכל thread בודק אם האינדקס שהוא קיבל בכלל קיים בתחום הנתונים, לפני שהוא ניגש אליהם.
הטעות הכי נפוצה של מתחילים היא לשכוח את הבדיקה הזו. בלי בדיקת גבולות, threads ב-block האחרון עלולים לגשת לזיכרון שלא שייך להם, מה שיכול לגרום לתוצאות שגויות או לקריסה של התוכנית כולה.
מבנים תלת-ממדיים¶
לא כל בעיה היא רשימה חד-ממדית. אם אתם מעבדים מטריצה, נוח הרבה יותר לחשוב על ה-grid וה-block כמבנה דו-ממדי, שבו כל thread מקבל שורה ועמודה במקום אינדקס בודד. CUDA תומכת בזה באופן טבעי, וזה הופך את הקוד לקריא הרבה יותר - במקום לחשב שורה ועמודה מתוך אינדקס שטוח בעצמכם, אתם מקבלים אותם ישירות מהחומרה.
מה קורה כשמשנים את הקונפיגורציה¶
חלק מהניסיון שנצבר בכתיבת CUDA הוא פשוט לנסות כמה קונפיגורציות שונות על אותו kernel ולמדוד את התוצאה. שינוי גודל ה-block יכול לשנות את הביצועים בצורה משמעותית, בגלל ההשפעה על occupancy, ניצול shared memory, ודפוסי הגישה לזיכרון. אין נוסחת קסם אחת שמתאימה לכל kernel, וזו בדיוק הסיבה שכלי פרופיילינג הם חלק בלתי נפרד מהעבודה.
זה בדיוק סוג האינטואיציה שאנחנו בונים בקורס תכנות GPU - לא רק איך לכתוב launch configuration תקינה, אלא איך לבחור אחת שבאמת מנצלת את החומרה.
לסיכום¶
launch configuration היא ההחלטה שקובעת כמה threads רצים ואיך הם מאורגנים, והיא הרבה יותר מפרט טכני שצריך למלא. גודל block נכון, חישוב grid נכון, ובדיקת גבולות בתוך ה-kernel, הם הבסיס שעליו נבנה כל קוד GPU תקין ומהיר.
בואו תלמדו לכייל את זה נכון, עם קהילה שכבר שם.